👏 Идеальное решение для написания bash-скриптов на nodejs!

Node.js JavaScript

предисловие

недавно учусьbashСинтаксис скрипта, но если вы не знакомы с синтаксисом bash, он кажется очень подверженным ошибкам, например: отображать неопределенные переменныеshellПеременная не определена и все еще может использоваться, но ее результаты могут быть не такими, как вы ожидали. Например:

#!/bin/bash

# 这里是判断变量var是否等于字符串abc,但是var这个变量并没有声明
if [ "$var" = "abc" ] 
then
   # 如果if判断里是true就在控制台打印 “ not abc”
   echo  " not abc" 
else
   # 如果if判断里是false就在控制台打印 “ abc”
   echo " abc "
fi

В результате печатается abc, но проблема в том, что этот скрипт должен сообщать об ошибке, это ошибка, если переменной не присвоено значение.

Чтобы восполнить эти ошибки, учимся добавлять в начало скрипта:set -uЭта команда означает, что скрипт добавляет ее в голову, и он сообщит об ошибке и прекратит выполнение при встрече с несуществующей переменной.

Повторный запуск предложит: test.sh: 3: test.sh: num: параметр не установлен

Представьте еще раз, что вы изначально хотели удалить:rm -rf $dir/*Тогда, когда каталог пуст, что становится?rm -rfэто команда удаления,$dirЕсли он пуст, это эквивалентно выполнениюrm -rf /*, то есть удалить все файлы и папки. . . Значит, ваша система пропала, это легендарное удаление библиотеки?~~~

еслиnodeИли среду браузера, мы напрямуюvar === 'abc'Он обязательно сообщит об ошибке, а это означает, что большой опыт программирования на javascript нельзя использовать повторно.bashДа ладно, было бы здорово, если бы его можно было использовать повторно.

Позже я начал исследовать, если вы используетеnodeсценарий вместо этогоbashКак хорошо, после суток метания и метания я постепенно нашел артефакт,GoogleэтоzxБиблиотека, не волнуйтесь, я не буду сначала представлять эту библиотеку, давайте посмотрим на текущее массовое использование.nodeкак писатьbashСкрипт, вы знаете, почему это артефакт.

node выполнить скрипт bash: неохотное решение: API child_process

Напримерchild_processвнутри APIexecЗаказ

const { exec } = require("child_process");

exec("ls -la", (error, stdout, stderr) => {
    if (error) {
        console.log(`error: ${error.message}`);
        return;
    }
    if (stderr) {
        console.log(`stderr: ${stderr}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
});

Здесь следует отметить, что сначалаexecасинхронно, но мыbashМногие команды сценария являются синхронными.

И обратите внимание:errorобъект отличается отstderr. errorкогдаchild_processКогда модуль не может выполнить команду, объект не является нулевым. Например, поиск файла не может найти файл, объект ошибки не пуст. Однако, если команда выполняется успешно и записывает сообщения в стандартный поток ошибок,stderrОбъект не будет нулевым.

Конечно, мы можем использовать синхронныйexecЗаказ,execSync

// 引入 exec 命令 from child_process 模块
const { execSync } = require("child_process");

// 同步创建了一个hello的文件夹
execSync("mkdir hello");

Давайте кратко представим другие API-интерфейсы child_process, которые могут выполнять команды bash.

  • spawn: запустить дочерний процесс для выполнения команды
  • exec: запускает дочерний процесс для выполнения команд.В отличие от spawn, он имеет функцию обратного вызова, чтобы узнать статус дочернего процесса.
  • execFile: запустить дочерний процесс для выполнения исполняемого файла
  • fork: похож на spawn, разница в том, что он должен указать файл javascript, который должен выполнить дочерний процесс.

Разница между exec и ececFile в том, что exec подходит для выполнения команд, а eexecFile подходит для выполнения файлов.

Node выполняет bash-скрипты: передовые решения shelljs

const shell = require('shelljs');
 
# 删除文件命令
shell.rm('-rf', 'out/Release');
// 拷贝文件命令
shell.cp('-R', 'stuff/', 'out/Release');
 
# 切换到lib目录,并且列出目录下到.js结尾到文件,并替换文件内容(sed -i 是替换文字命令)
shell.cd('lib');
shell.ls('*.js').forEach(function (file) {
  shell.sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
  shell.sed('-i', /^.*REMOVE_THIS_LINE.*$/, '', file);
  shell.sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, shell.cat('macro.js'), file);
});
shell.cd('..');
 
# 除非另有说明,否则同步执行给定的命令。 在同步模式下,这将返回一个 ShellString
#(与 ShellJS v0.6.x 兼容,它返回一个形式为 { code:..., stdout:..., stderr:... } 的对象)。
# 否则,这将返回子进程对象,并且回调接收参数(代码、标准输出、标准错误)。
if (shell.exec('git commit -am "Auto-commit"').code !== 0) {
  shell.echo('Error: Git commit failed');
  shell.exit(1);
}

Судя по приведенному выше коду, shelljs действительно является очень хорошим решением для nodejs для написания сценариев bash.Если ваша среда узла не может быть случайно обновлена, я думаю, что shelljs действительно достаточно.

Тогда давайте взглянем на сегодняшнюю протагонистку zx, старт уже 17,4к.

zx-библиотека

Официальный сайт:www.npmjs.com/package/zx

Давайте посмотрим, как использовать

#!/usr/bin/env zx

await $`cat package.json | grep name`

let branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`

await Promise.all([
  $`sleep 1; echo 1`,
  $`sleep 2; echo 2`,
  $`sleep 3; echo 3`,
])

let name = 'foo bar'
await $`mkdir /tmp/${name}

Как вы думаете? Вы просто пишете команды для Linux? Вы можете игнорировать большую часть синтаксиса bash и просто использовать js напрямую, и его преимущества не ограничиваются этим. Есть несколько интересных особенностей:

1. Поддержка ts, автоматическая компиляция .ts в файлы .mjs.Файл .mjs является концом файла, который поддерживает модуль es6, который поставляется с более высокой версией узла.То есть этот файл может напрямую импортировать модуль без экранирования другие инструменты.

2. Встроенная поддержка метода трубопровода трубопровода

3. У него есть собственная библиотека выборки, которая может делать сетевые запросы, собственная библиотека мела, которая может печатать цветные шрифты, и собственный метод обработки ошибок nothrow.Если команда bash делает ошибку, ее можно обернуть в этот метод, чтобы игнорировать ошибку

Полный китайский документ (уровень перевода ниже средний, пожалуйста, простите меня)

#!/usr/bin/env zx

await $`cat package.json | grep name`

let branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`

await Promise.all([
  $`sleep 1; echo 1`,
  $`sleep 2; echo 2`,
  $`sleep 3; echo 3`,
])

let name = 'foo bar'
await $`mkdir /tmp/${name}

Bash великолепен, но при написании скриптов люди обычно выбирают более удобный язык программирования. JavaScript — идеальный выбор, но стандартная библиотека Node.js требует нескольких дополнительных действий, прежде чем ее можно будет использовать. zx основан на child_process , экранирует аргументы и предоставляет разумные значения по умолчанию.

Установить

npm i -g zx

требуемая среда

Node.js >= 14.8.0

Напишите скрипт с расширением.mjsфайл, который можно использовать на верхнем уровнеawait.

поставить следующееshebangдобавить вzxНачало скрипта:

#!/usr/bin/env zx
现在您将能够像这样运行您的脚本:

chmod +x ./script.mjs
./script.mjs

или черезzxзапускаемый файл:

zx ./script.mjs

все функции($、cd、fetch 等)можно использовать напрямую без импорта.

$`command`

использоватьchild_processФункция spawn в пакете выполняет заданную строку и возвращает ProcessPromise.

let count = parseInt(await $`ls -1 | wc -l`)
console.log(`Files count: ${count}`)

Например, чтобы загружать файлы параллельно:

Если исполняемая программа возвращает ненулевой код выхода,ProcessOutputбудет брошен

try {
  await $`exit 1`
} catch (p) {
  console.log(`Exit code: ${p.exitCode}`)
  console.log(`Error: ${p.stderr}`)
}

ProcessPromise, ниже приведено определение интерфейса машинописного текста обещания.

class ProcessPromise<T> extends Promise<T> {
  readonly stdin: Writable
  readonly stdout: Readable
  readonly stderr: Readable
  readonly exitCode: Promise<number>
  pipe(dest): ProcessPromise<T>
}

pipe()методы могут использоваться для перенаправления стандартного вывода:

await $`cat file.txt`.pipe(process.stdout)

Подробнее о трубопроводахGitHub.com/Google/these/no…

ProcessOutputизtypescriptОпределение интерфейса

class ProcessOutput {
  readonly stdout: string
  readonly stderr: string
  readonly exitCode: number
  toString(): string
}

функция:

cd()

Изменить текущий рабочий каталог

cd('/tmp')
await $`pwd` // outputs /tmp

fetch()

пакет извлечения узла.

let resp = await fetch('http://wttr.in')
if (resp.ok) {
  console.log(await resp.text())
}

question()

пакет readline

let bear = await question('What kind of bear is best? ')
let token = await question('Choose env variable: ', {
  choices: Object.keys(process.env)
})

Во втором параметре можно указать массив опций для автозаполнения вкладок

Ниже приведено определение интерфейса

function question(query?: string, options?: QuestionOptions): Promise<string>
type QuestionOptions = { choices: string[] }

sleep()

На основе функции setTimeout

await sleep(1000)

nothrow()

Измените поведение $, чтобы не создавать исключение, если код выхода не равен 0.

определение интерфейса ts

function nothrow<P>(p: P): P
await nothrow($`grep something from-file`)
// 在管道内:

await $`find ./examples -type f -print0`
  .pipe(nothrow($`xargs -0 grep something`))
  .pipe($`wc -l`)

Следующие пакеты не нужно импортировать, их можно использовать напрямую.

chalk

console.log(chalk.blue('Hello world!'))

fs

Подобно следующему использованию

import {promises as fs} from 'fs'
let content = await fs.readFile('./package.json')

os

await $`cd ${os.homedir()} && mkdir example`

Конфигурация:

$.shell

Указывает используемый bash.

$.shell = '/usr/bin/bash'

$.quote

Указывает функцию, используемую для экранирования специальных символов во время подстановки команд.

Пакет shq используется по умолчанию.

Уведомление:

__filename & __dirnameЭти две переменныеcommonjsсередина. мы используем.mjsокончаниеes6модуль.

существуетESMМодуль,Node.jsНе предоставлен__filenameа также __dirnameглобальные переменные. Так как такие глобальные переменные очень удобны в скриптах, поэтому zxОни предоставляются.mjsиспользуемый файл (при использованииzxзапускаемый файл)

requireСлишкомcommonjsМетод модуля импорта в , существуетESMмодуль, не определеноrequire()функция.zxпри условииrequire()функция, поэтому ее можно использовать с.mjsфайл используется вместе с импортами (при использованииzx запускаемый файл)

Передать переменные среды

process.env.FOO = 'bar'
await $`echo $FOO`

передать массив

Если массив значений передается в качестве аргумента $, элементы массива будут экранированы по отдельности и соединены пробелами. Пример:

let files = [1,2,3]
await $`tar cz ${files}`

$ и другие функции могут использоваться с явным импортом

#!/usr/bin/env node
import {$} from 'zx'
await $`date`

zx может компилировать сценарии .ts в .mjs и выполнять их

zx examples/typescript.ts