“Bash is good, so I choose JavaScript”

We spend most of our daily front-end development time in Javascript, and we’re not as familiar with writing bash as WE are with Javascript when writing shell scripts, so why not choose a more convenient programming language to write it in? The Google ZX tool makes this possible. It has been successfully introduced on GitHub as a more convenient and friendly tool to help developers write scripts.

How does it work?

Install and initialize (NPM i-g zx), Node version restriction (>= 14.8.0)

// demo.mjs
await Promise.all([
  $`sleep 1; echo 3`,
  $`sleep 2; echo 2`,
  $`sleep 3; echo 1`,]),let tName = 'google/zx'
await $`echo '/hello/${tName}'`
Copy the code

The command line executes zx demo.mjs with a 3-second countdown to see /hello/ Google /zx

That’s a simple example of how to use it, but let’s get down to business

What capabilities are provided?

Three ways to use it

zx << ‘EOF’
if (typeof firstArg === 'undefined' || firstArg === The '-') {... }Copy the code

If the input command does not have firstArg, the logic will be handled by scriptFromStdin:

  • Use process.stdin.istty to determine whether the terminal condition is present

  • If not, set the UTF8 character encoding

  • Loop and concatenate the input line by line

  • Create a randomly named MJS file in the temporary folder

Math.random().toString(36).substr(2) + ‘.mjs’ for example:

zx <<'EOF'
await $`pwd`
EOF
Copy the code
Pull script execution from remote end
if (firstArg.startsWith('http://') || firstArg.startsWith('https://')) { ... }
Copy the code

Pull data is mainly realized through scriptFromHttp, which includes the following steps:

  • Use the Node-FETCH package to fetch the data of the corresponding URL

  • When the data is pulled, it is named by the PATHName of the URL

  • Create files in a temporary folder

Local script file execution

If the preceding two judgments are not entered, the following logic is executed: if the link begins with a /, the link is used directly; if file begins with a call to url.fileurltopath to obtain the absolute path string; if the other logic is path.resolve, the path is obtained

if (firstArg.startsWith('/')) {
  filepath = firstArg
} else if (firstArg.startsWith('file:///')) {
  filepath = url.fileURLToPath(firstArg)
} else {
  filepath = resolve(firstArg)
}
Copy the code

Supports multiple file formats importPath

Empty suffix

In this case, zX directly reads the file information by readFile, obtains the current file directory and filename, writes a new MJS file, and enters the importPath method again with import execution

Markdown files

Zx can also execute the scripts written by Markdow. Only the parts wrapped in code are parsed. The transformMarkdown method is used to cut into arrays with \ N and then scan them line by line and match them with the re. Finally, the data is converted into a string as script data by concatenating the newline character \n, and the file is generated in the same way as the Markdown format example above:

# Markdown Scripts

It's possible to write scripts using markdown. Only code blocks will be executed
by zx. 

> You can run this markdown file:
> > ` ` `
> zx docs/markdown.md
> ` ` `

```js
await $`whoami`
await $`echo ${__dirname}`
Copy the code

The __filename will be pointed to markdown.md:

console.log(chalk.yellowBright(__filename))
Copy the code

We can use imports here as well:

await import('chalk')
Copy the code

A bash code (with bash or sh language tags) also will be executed:

VAR=$(date)
echo "$VAR" | wc -c
Copy the code

Other code blocks are ignored:

body .hero {
    margin: 42px;
}
Copy the code
##### Ts file core part is to execute 'compile' method for TSC to compile a JS, Let TSC = $' nPM_config_YES =true NPX -p typescript TSC --target esNext --lib esnext --module  esnext --moduleResolution node ${input}` await tscCopy the code

There is a loading, which is implemented by setInterval

let i = 0
let interval = setInterval(() = > {
process.stdout.write(' '
  + ['⠋'.'⠙'.'⠹'.'⠸'.'⠼'.'⠴'.'⠦'.'⠧'.'⠇'.'⠏'][i++ % 10]
  + '\r')},100)...clearInterval(interval)
Copy the code

The $function

The first is the ability to handle ARGs using tag functions

If an array is passed in, the array is concatenated, for example:

let flags = [
  '--oneline'.'--decorate'.'--color',]await $`git log ${flags}`

// zx handles the logic by formatting it first and then concatenating it with a space
if (Array.isArray(args[i])) {
  s = args[i].map(x= > $.quote(substitute(x))).join(' ')}Copy the code

The _fun method is triggered when CMD concatenates the translated ARgs and creates an instance of the ProcessPromise class that calls then

 then(onfulfilled, onrejected) {
    if (this._run) this._run()
    return super.then(onfulfilled, onrejected)
 }
Copy the code

The core of the _run method is child_process.spawn

Spawn: starts a child process to execute commands exec: starts a child process to execute commands. Unlike spawn, it has a callback function that knows the status of the child process execFile: Fork: Starts a child process to execute the executable. Fork: is similar to spawn except that it specifies the module that the child process needs to execute

Child_process listens for exit and close events, and then outputs an output with a code of 0 indicating success, or an error message in the exitCodeInfo method if it fails


function exitCodeInfo(exitCode) {
  return {
    2: 'Misuse of shell builtins',
    126: 'Invoked command cannot execute',
    127: 'Command not found',
    128: 'Invalid exit argument',
    129: 'Hangup',
    130: 'Interrupt',
    131: 'Quit and dump core',
    132: 'Illegal instruction',
    133: 'Trace/breakpoint trap',
    134: 'Process aborted',
    135: 'Bus error: "access to undefined portion of memory object"',
    136: 'Floating point exception: "erroneous arithmetic operation"',
    137: 'Kill (terminate immediately)',
    138: 'User-defined 1',
    139: 'Segmentation violation',
    140: 'User-defined 2',
    141: 'Write to pipe with no one reading',
    142: 'Signal raised by alarm',
    143: 'Termination (request to terminate) ',145: 'Child process terminated, stopped (or continued*)',
    146: 'Continue if stopped',
    147: 'Stop executing temporarily',
    148: 'Terminal stop signal',
    149: 'Background process attempting to read from tty ("in") ',150: 'Background process attempting to write to tty ("out") ',151: 'Urgent data available on socket',
    152: 'CPU time limit exceeded',
    153: 'File size limit exceeded',
    154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
    155: 'Profiling timer expired',
    157: 'Pollable event',
    159: 'Bad syscall',
  }[exitCode]
}
Copy the code

Other Funtions

cd()

Change the current working directory with $.cwd set

if(! fs.existsSync(path)) {let __from = (new Error().stack.split(/^\s*at\s/m) [2]).trim()
    console.error(`cd: ${path}: No such directory`)
    console.error(`    at ${__from}`)
    process.exit(1)}Copy the code
fetch()

Node-fetch is actually used to pull data

nodeFetch(url: RequestInfo, init? : RequestInit): Promise<Response>Copy the code
question()

Readline syntax sugar supports select mode by passing in an array by setting options.choices

sleep()

Use util.promisify to wrap a layer of setTimeout and return a promise, such as await sleep(1000) callback style is more elegant

nothrow()

Set _nothrow to true, child Process listens for close events, and returns the code! == 0 can also resolve

Examples:

await nothrow($`grep something from-file`)
Copy the code

Packages that can be used directly

chalk package
console.log(chalk.blue('Hello world! '))
Copy the code
fs package ( fs-extra )
let content = await fs.readFile('./package.json')
Copy the code
globby package

This package provides methods to traverse the file system and return pathnames that match the defined set of specified patterns according to the rules used by the UnixBashshell, while returning results in any order, making it fast, simple, and efficient to use.

let packages = await globby(['package.json'.'packages/*/package.json'])
let pictures = globby.globbySync('content/*.(jpg|png)')
// Also, globby available via the glob shortcut:
await $`svgo The ${await glob('*.svg')}`
Copy the code
os package

Provides operating system-specific methods and properties

await $`cd ${os.homedir()} && mkdir example`
Copy the code
minimist package
// Example
$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo'.'bar'.'baz'].x: 3.y: 4.n: 5.a: true.b: true.c: true.beep: 'boop' }
Copy the code

Parameter configuration

$.quote

Here is the main implementation of a quote method, mainly used for special character translation

function quote(arg) {
  if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === ' ') {
    return arg
  }
  return ` $' `
    + arg
      .replace(/\\/g.'\ \ \ \')
      .replace(/'/g.'\ \ \')
      .replace(/\f/g.'\\f')
      .replace(/\n/g.'\\n')
      .replace(/\r/g.'\\r')
      .replace(/\t/g.'\\t')
      .replace(/\v/g.'\\v')
      .replace(/\0/g.'\ \ 0')
    + ` '`
}
Copy the code
$verbose (– quiet)

$. Verbose = false $. Verbose = false $. Verbose = false $.

  • CD command to prompt the current path

  • Remote pull script that prompts the URL of the current fetch

$.cwd

The default is undefined. $. CWD can be set using the CD method to get the current working directory

$. Shell (shell)

Specify the shell to use. For example:

$.shell = '/usr/bin/bash'
Copy the code
$.prefix

Commands to set the prefixes of all run commands default to -euo pipefail or use –prefix, for example: –prefix=’set -e; ‘

Polyfills

Nodejs does not provide __dirname and __filename in ESM, and does not define require(), so zX has these variables in global for the convenience of users

  let __filename = resolve(origin)
  let __dirname = dirname(__filename)
  let require = createRequire(origin)
  Object.assign(global, {__filename, __dirname, require})
Copy the code

That’s all for this introduction