Создание сайтов в Золотом, ЛНР. Как писать сценарии оболочки в узле с помощью библиотеки zx от Google

В этой статье мы узнаем, что предоставляет библиотека Google zx и как мы можем использовать ее для написания сценариев оболочки с помощью Node.js. Затем мы узнаем, как использовать функции zx, создав инструмент командной строки, который поможет нам настроить начальную конфигурацию для новых проектов Node.js.

Написание сценариев оболочки: проблема

Создание сценария оболочки — сценария, который выполняется такой оболочкой, как Bash или zsh, — может быть отличным способом автоматизации повторяющихся задач. Node.js кажется идеальным выбором для написания сценария оболочки, поскольку он предоставляет нам ряд основных модулей и позволяет нам импортировать любую выбранную нами библиотеку. Это также дает нам доступ к функциям языка и встроенным функциям, предоставляемым JavaScript.

Но если вы пытались написать сценарий оболочки для работы под Node.js, вы, вероятно, обнаружили, что это не так гладко, как хотелось бы. Вам нужно написать специальную обработку для дочерних процессов, позаботиться об экранировании аргументов командной строки, а затем в конечном итоге возиться с stdout (стандартным выводом) и stderr (стандартной ошибкой). Это не особенно интуитивно понятно и может сделать написание сценариев оболочки довольно неудобным.

Язык сценариев оболочки Bash является популярным выбором для написания сценариев оболочки. Нет необходимости писать код для обработки дочерних процессов, и он имеет встроенные функции языка для работы с stdoutи stderr. Но писать сценарии оболочки с помощью Bash тоже не так просто. Синтаксис может быть довольно запутанным, что затрудняет реализацию логики или обработку таких вещей, как запрос пользовательского ввода.

Библиотека zx от Google помогает сделать сценарии оболочки с помощью Node.js эффективными и приятными.

Требования к сопровождению

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

В идеале вы должны быть знакомы с основами JavaScript и Node.js.

Вам нужно уметь запускать команды в терминале.

Вам потребуется установить Node.js ≥ v14.13.1.

Весь код в этой статье доступен на GitHub.

Как работает zx от Google?

Zx от Google предоставляет функции, которые завершают создание дочерних процессов, а также обработку этих процессов stdoutи stderrиз них. Основная функция, с которой мы будем работать, — это $функция. Вот пример этого в действии:

import { $ } from «zx»;

await $`ls`;

И вот результат выполнения этого кода:

$ ls

bootstrap-tool

hello-world

node_modules

package.json

README.md

typescript

Синтаксис JavaScript в приведенном выше примере может показаться немного странным. Он использует языковую функцию, называемую помеченными литералами шаблонов. Функционально это то же самое, что писать await $ («ls»).

Zx от Google предоставляет несколько других служебных функций, упрощающих создание сценариев оболочки, например:

cd (). Это позволяет нам изменить наш текущий рабочий каталог.

question (). Это оболочка для модуля чтения Node.js. Это упрощает запрос ввода данных пользователем.

Помимо служебных функций, которые предоставляет zx, он также предоставляет нам несколько популярных библиотек, таких как:

мел. Эта библиотека позволяет нам добавлять цвет к выходным данным наших скриптов.

минималистский. Библиотека, анализирующая аргументы командной строки. Затем они выставляются подargvобъектом.

принести. Популярная реализация Fetch API в Node.js. Мы можем использовать его для выполнения HTTP-запросов.

фс-экстра. Библиотека, предоставляющая основной fs-модуль Node.js, а также ряд дополнительных методов, облегчающих работу с файловой системой.

Теперь, когда мы знаем, что дает нам zx, давайте создадим с его помощью наш первый сценарий оболочки.

Привет, мир с zx от Google

Сначала создадим новый проект:

mkdir zx-shell-scripts

cd zx-shell-scripts

npm init —yes

Затем мы можем установить zxбиблиотеку:

npm install —save-dev zx

Примечание: в zxдокументации предлагается установить библиотеку глобально с помощью npm. Вместо этого, установив его как локальную зависимость нашего проекта, мы можем гарантировать, что zx всегда установлен, а также контролировать версию, которую используют наши сценарии оболочки.

Высший уровеньawait

Чтобы использовать верхний уровень awaitв Node.js — awaitвне asyncфункции — нам нужно написать наш код в модулях ECMAScript (ES), которые поддерживают верхний уровень await. Мы можем указать, что все модули в проекте являются модулями ES, добавив «type»: «module»в наш package.json, или мы можем установить расширение файла отдельных скриптов на.mjs. Мы будем использовать.mjsрасширение файла для примеров в этой статье.

Запуск команды и захват ее вывода

Давайте создадим новый скрипт с именем hello-world.mjs. Мы добавим строку shebang, которая сообщает ядру операционной системы (ОС) запустить скрипт с nodeпрограммой:

#! /usr/bin/env node

Теперь мы добавим код, использующий zx для запуска команды.

В следующем коде мы запускаем команду для выполнения программы ls. Программа lsвыведет список файлов в текущем рабочем каталоге (каталоге, в котором находится скрипт). Мы захватим стандартный вывод процесса команды, сохраним его в переменной и затем выведем в терминал:

// hello-world.mjs

import { $ } from «zx»;

const output = (await $`ls`).stdout;

console.log (output) ;

Примечание: в zxдокументации предлагается вставить строку shebang в наши скрипты, но вместо этого /usr/bin/env zxмы используем. /usr/bin/env nodeЭто потому, что мы установили zxкак локальную зависимость нашего проекта. zxЗатем мы явно импортируем из пакета функции и объекты, которые хотим использовать. Это помогает понять, откуда берутся зависимости, используемые в нашем скрипте.

Затем мы будем использовать chmod, чтобы сделать скрипт исполняемым:

chmod u+x hello-world.mjs

Запустим наш скрипт:

. /hello-world.mjs

Теперь мы должны увидеть следующий вывод:

$ ls

hello-world.mjs

node_modules

package.json

package-lock.json

README.md

hello-world.mjs

node_modules

package.json

package-lock.json

README.md

Вы заметите несколько вещей в выводе нашего сценария оболочки:

Выполненная нами команда (ls) включена в вывод.

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

В конце вывода есть дополнительная новая строка.

zxработает в verboseрежиме по умолчанию. Он выведет команду, которую вы передаете $функции, а также выведет стандартный вывод этой команды. Мы можем изменить это поведение, добавив следующую строку кода перед запуском lsкоманды:

$.verbose = false;

Большинство программ командной строки, таких как ls, выводят символ новой строки в конце своего вывода, чтобы сделать вывод более читаемым в терминале. Это хорошо для удобочитаемости, но поскольку мы сохраняем вывод в переменной, нам не нужна эта дополнительная новая строка. Мы можем избавиться от него с помощью функции JavaScript String#trim ():

— const output = (await $`ls`).stdout;

+ const output = (await $`ls`).stdout.trim () ;

Если мы снова запустим наш скрипт, мы увидим, что все выглядит намного лучше:

hello-world.mjs

node_modules

package.json

package-lock.json

Использование zx от Google с TypeScript

Если мы хотим писать сценарии оболочки, использующие zxTypeScript, необходимо учитывать несколько незначительных отличий.

Примечание. Компилятор TypeScript предоставляет ряд параметров конфигурации, которые позволяют нам настроить способ компиляции нашего кода TypeScript. Имея это в виду, следующая конфигурация и код TypeScript предназначены для работы в большинстве версий TypeScript.

Во-первых, давайте установим зависимости, которые нам понадобятся для запуска нашего кода TypeScript:

npm install —save-dev typescript ts-node

Пакет ts-node предоставляет механизм выполнения TypeScript, позволяющий нам транспилировать и запускать код TypeScript.

Нам нужно создать tsconfig.jsonфайл, содержащий следующую конфигурацию:

{

«compilerOptions»: {

«target»: «es2017»,

«module»: «commonjs»

}

}

Давайте теперь создадим новый скрипт с именем hello-world-typescript.ts. Во-первых, мы добавим строку shebang, которая сообщает ядру нашей ОС запустить скрипт с ts-nodeпрограммой:

#!. /node_modules/.bin/ts-node

Чтобы использовать awaitключевое слово в нашем коде TypeScript, нам нужно обернуть его в немедленно вызываемое функциональное выражение (IIFE), как рекомендовано в документации zx:

// hello-world-typescript.ts

import { $ } from «zx»;

void (async function () {

await $`ls`;

}) () ;

Затем нам нужно сделать скрипт исполняемым, чтобы мы могли выполнить его напрямую:

chmod u+x hello-world-typescript.ts

Когда мы запускаем скрипт:

. /hello-world-typescript.ts

...мы должны увидеть следующий вывод:

$ ls

hello-world-typescript.ts

node_modules

package.json

package-lock.json

README.md

tsconfig.json

Написание скриптов zxна TypeScript похоже на использование JavaScript, но требует небольшой дополнительной настройки и упаковки нашего кода.

Создание инструмента начальной загрузки проекта

Теперь, когда мы изучили основы написания сценария оболочки с помощью Google zx, мы собираемся создать с его помощью инструмент. Этот инструмент автоматизирует создание процесса, который часто занимает много времени: начальной загрузки конфигурации для нового проекта Node.js.

Мы собираемся создать интерактивный сценарий оболочки, который запрашивает ввод данных пользователем. Он также будет использовать chalkбиблиотеку, которая zxвыделяет выходные данные разными цветами и обеспечивает удобный пользовательский интерфейс. Наш сценарий оболочки также установит пакеты npm, которые нужны нашему новому проекту, поэтому он готов для того, чтобы мы могли сразу начать разработку.

Начиная

Давайте создадим новый файл с именем bootstrap-tool.mjsи добавим строку shebang. Мы также импортируем из пакета функции и модули, которые будем использовать zx, а также основной pathмодуль Node.js:

#! /usr/bin/env node

// bootstrap-tool.mjs

import { $, argv, cd, chalk, fs, question } from «zx»;

import path from «path»;

Как и в случае со скриптами, которые мы создали ранее, мы хотим сделать наш новый скрипт исполняемым:

chmod u+x bootstrap-tool.mjs

Мы также собираемся определить вспомогательную функцию, которая выводит сообщение об ошибке красным текстом и завершает процесс Node.js с кодом завершения ошибки 1:

function exitWithError (errorMessage) {

console.error (chalk.red (errorMessage));

process.exit (1) ;

}

Мы будем использовать эту вспомогательную функцию в разных местах нашего сценария оболочки, когда нам нужно обработать ошибку.

Проверить зависимости

Создаваемому нами инструменту потребуется запускать команды, использующие три разные программы: git, nodeи npx. Мы можем использовать библиотеку, которая поможет нам проверить, установлены ли эти программы и доступны ли они для использования.

Во-первых, нам нужно установить whichпакет:

npm install —save-dev which

Затем мы можем импортировать его:

import which from «which»;

Затем мы создадим checkRequiredProgramsExistфункцию, которая его использует:

async function checkRequiredProgramsExist (programs) {

try {

for (let program of programs) {

await which (program) ;

}

} catch (error) {

exitWithError (`Error: Required command ${error.message}`) ;

}

}

Приведенная выше функция принимает массив имен программ. Он перебирает массив и для каждой программы вызывает whichфункцию. Если whichнайдет путь к программе, то вернет его. В противном случае, если программа отсутствует, она выдаст ошибку. Если какая-либо из программ отсутствует, мы вызываем нашего exitWithErrorпомощника, чтобы вывести сообщение об ошибке и остановить выполнение скрипта.

Теперь мы можем добавить вызов для checkRequiredProgramsExistпроверки доступности программ, от которых зависит наш инструмент:

await checkRequiredProgramsExist ([«git», «node», «npx»]) ;

Добавить параметр целевого каталога

Поскольку инструмент, который мы создаем, поможет нам запускать новые проекты Node.js, нам нужно запускать любые команды, которые мы добавляем в каталог проекта. Теперь мы собираемся добавить —directoryаргумент командной строки в наш скрипт.

zxвключает в себя пакет minimist, который анализирует любые аргументы командной строки, передаваемые нашему сценарию. Эти проанализированные аргументы командной строки доступны argvв zxпакете.

Давайте добавим проверку для аргумента командной строки с именем directory:

let targetDirectory = argv.directory;

if (! targetDirectory) {

exitWithError («Error: You must specify the —directory argument») ;

}

Если directoryаргумент был передан нашему сценарию, мы хотим проверить, что это путь к существующему каталогу. Мы будем использовать fs.pathExistsметод, предоставленный fs-extra:

targetDirectory = path.resolve (targetDirectory) ;

if (! (await fs.pathExists (targetDirectory))) {

exitWithError (`Error: Target directory '${targetDirectory}' does not exist`) ;

}

Если целевой каталог существует, мы будем использовать cdфункцию, предоставленную zxдля изменения нашего текущего рабочего каталога:

cd (targetDirectory) ;

Если мы сейчас запустим наш скрипт без —directoryаргумента, мы должны получить ошибку:

$. /bootstrap-tool.mjs

Error: You must specify the —directory argument

Проверьте глобальные настройки Git

Через мгновение мы собираемся инициализировать новый репозиторий Git в каталоге нашего проекта, но сначала мы хотим убедиться, что Git имеет необходимую конфигурацию. Мы хотим убедиться, что наши коммиты будут корректно атрибутированы сервисами размещения кода, такими как GitHub.

Для этого создадим getGlobalGitSettingValueфункцию. Он запустит команду git configдля получения значения параметра конфигурации Git:

async function getGlobalGitSettingValue (settingName) {

$.verbose = false;

let settingValue = «»;

try {

settingValue = (

await $`git config —global —get ${settingName}`

).stdout.trim () ;

} catch (error) {

// Ignore process output

}

$.verbose = true;

return settingValue;

}

Вы заметите, что мы выключаем verboseрежим, который zx установил по умолчанию. Это означает, что когда мы запускаем git configкоманды, команда и все, что она отправляет в стандартный вывод, не будет отображаться. Мы снова включаем подробный режим в конце функции, чтобы не влиять на другие команды, которые мы добавим позже в наш сценарий.

Теперь мы создадим объект checkGlobalGitSettings, который принимает массив имен настроек Git. Он будет перебирать каждое имя параметра и передавать его getGlobalGitSettingValueфункции для получения его значения. Если параметр не имеет значения, мы отобразим предупреждающее сообщение:

async function checkGlobalGitSettings (settingsToCheck) {

for (let settingName of settingsToCheck) {

const settingValue = await getGlobalGitSettingValue (settingName) ;

if (! settingValue) {

console.warn (

chalk.yellow (`Warning: Global git setting '${settingName}' is not set. `)

) ;

}

}

}

Давайте вызовем add call to checkGlobalGitSettingsи проверим, установлены ли настройки user.nameи Git: user.email

await checkGlobalGitSettings ([«user.name», «user.email»]) ;

Инициализировать новый репозиторий Git

Мы можем инициализировать новый репозиторий Git в каталоге проекта, добавив следующую команду:

await $`git init`;

Создать package.jsonфайл

Каждому проекту Node.js нужен package.jsonфайл. Здесь мы определяем метаданные о проекте, указываем пакеты, от которых зависит проект, и добавляем небольшие служебные сценарии.

Прежде чем сгенерировать package.jsonфайл для нашего проекта, мы создадим пару вспомогательных функций. Первая — это readPackageJsonфункция, которая будет читать package.jsonфайл из каталога проекта:

async function readPackageJson (directory) {

const packageJsonFilepath = `${directory}/package.json`;

return await fs.readJSON (packageJsonFilepath) ;

}

Затем мы создадим функцию, которую мы можем использовать для записи изменений в файл writePackageJsonпроекта: package.json

async function writePackageJson (directory, contents) {

const packageJsonFilepath = `${directory}/package.json`;

await fs.writeJSON (packageJsonFilepath, contents, { spaces: 2 }) ;

}

Методы fs.readJSONи fs.writeJSON, которые мы использовали в приведенных выше функциях, предоставляются fs-extraбиблиотекой.

Определив наши package.jsonвспомогательные функции, мы можем начать думать о содержимом нашего package.jsonфайла.

Node.js поддерживает два типа модулей:

Модули CommonJS (CJS). Используетсяmodule.exportsдля экспорта функций и объектов иrequire () их загрузки в другой модуль.

Модули ECMAScript (ESM). Используетсяexportдля экспорта функций и объектов иimportих загрузки в другой модуль.

Экосистема Node.js постепенно внедряет модули ES, которые распространены в клиентском JavaScript. Пока все находится на этом переходном этапе, нам нужно решить, будут ли наши проекты Node.js использовать модули CJS или ESM по умолчанию. Давайте создадим promptForModuleSystemфункцию, которая спрашивает, какой тип модуля должен использовать этот новый проект:

async function promptForModuleSystem (moduleSystems) {

const moduleSystem = await question (

`Which Node.js module system do you want to use? (${moduleSystems.join (

«or „

) }) `,

{

choices: moduleSystems,

}

) ;

return moduleSystem;

}

Приведенная выше функция использует questionфункцию, предоставляемую zx.

Теперь мы создадим getNodeModuleSystemфункцию для вызова нашей promptForModuleSystemфункции. Он проверит правильность введенного значения. Если это не так, он снова задаст вопрос: s

async function getNodeModuleSystem () {

const moduleSystems = [“module», «commonjs»];

const selectedModuleSystem = await promptForModuleSystem (moduleSystems) ;

const isValidModuleSystem = moduleSystems.includes (selectedModuleSystem) ;

if (! isValidModuleSystem) {

console.error (

chalk.red (

`Error: Module system must be either '${moduleSystems.join (

«' or '»

) }'\n`

)

) ;

return await getNodeModuleSystem () ;

}

return selectedModuleSystem;

}

Теперь мы можем сгенерировать файл нашего проекта, package.jsonвыполнив команду npm init:

await $`npm init —yes`;

Затем мы воспользуемся нашей readPackageJsonвспомогательной функцией для чтения только что созданного package.jsonфайла. Мы спросим, ​​какую модульную систему должен использовать проект, установим ее в качестве значения typeсвойства в packageJsonобъекте, а затем запишем обратно в package.jsonфайл проекта:

const packageJson = await readPackageJson (targetDirectory) ;

const selectedModuleSystem = await getNodeModuleSystem () ;

packageJson.type = selectedModuleSystem;

await writePackageJson (targetDirectory, packageJson) ;

Совет: чтобы получить разумные значения по умолчанию package.jsonпри запуске npm initс —yesфлагом, убедитесь, что вы установили init-* параметры конфигурации npm.

Установите необходимые зависимости проекта

Чтобы упростить начало разработки проекта после запуска нашего инструмента начальной загрузки, мы создадим promptForPackagesфункцию, которая спрашивает, какие пакеты npm установить:

async function promptForPackages () {

let packagesToInstall = await question (

«Which npm packages do you want to install for this project? «

) ;

packagesToInstall = packagesToInstall

.trim ()

.split («»)

.filter ((pkg) => pkg) ;

return packagesToInstall;

}

На всякий случай, если мы получим опечатку при вводе имени пакета, мы создадим identifyInvalidNpmPackagesфункцию. Эта функция примет массив имен пакетов npm, а затем запустит команду npm view, чтобы проверить, существуют ли они:

async function identifyInvalidNpmPackages (packages) {

$.verbose = false;

let invalidPackages = [];

for (const pkg of packages) {

try {

await $`npm view ${pkg}`;

} catch (error) {

invalidPackages.push (pkg) ;

}

}

$.verbose = true;

return invalidPackages;

}

Давайте создадим getPackagesToInstallфункцию, которая использует две только что созданные функции:

async function getPackagesToInstall () {

const packagesToInstall = await promptForPackages () ;

const invalidPackages = await identifyInvalidNpmPackages (packagesToInstall) ;

const allPackagesExist = invalidPackages.length === 0;

if (! allPackagesExist) {

console.error (

chalk.red (

`Error: The following packages do not exist on npm: ${invalidPackages.join (

«, «

) }\n`

)

) ;

return await getPackagesToInstall () ;

}

return packagesToInstall;

}

Приведенная выше функция отобразит ошибку, если какие-либо имена пакетов неверны, а затем снова запросит установку пакетов.

Когда у нас есть список допустимых пакетов для установки, давайте установим их с помощью npm installкоманды:

const packagesToInstall = await getPackagesToInstall () ;

const havePackagesToInstall = packagesToInstall.length > 0;

if (havePackagesToInstall) {

await $`npm install ${packagesToInstall}`;

}

Создание конфигурации для инструментов

Создание конфигурации проекта — это идеальная вещь, которую мы можем автоматизировать с помощью нашего инструмента начальной загрузки проекта. Во-первых, давайте добавим команду для создания.gitignoreфайла, чтобы мы случайно не зафиксировали файлы, которые нам не нужны, в нашем репозитории Git:

await $`npx gitignore node`;

Приведенная выше команда использует пакет gitignore.gitignore для извлечения файла Node.js из шаблонов gitignore GitHub.

Чтобы сгенерировать наши файлы конфигурации EditorConfig, Prettier и ESLint, мы будем использовать инструмент командной строки под названием Mrm.

Давайте глобально установим mrmзависимости, которые нам понадобятся:

npm install —global mrm-task-editorconfigmrm-task-prettiermrm-task-eslint

Затем добавьте mrmкоманды для создания файлов конфигурации:

await $`npx mrm editorconfig`;

await $`npx mrm prettier`;

await $`npx mrm eslint`;

Mrm позаботится о создании файлов конфигурации, а также об установке необходимых пакетов npm. Он также предоставляет множество параметров конфигурации, что позволяет нам настраивать сгенерированные файлы конфигурации в соответствии с нашими личными предпочтениями.

Создайте базовый README

Мы можем использовать нашу readPackageJsonвспомогательную функцию, чтобы прочитать имя проекта из package.jsonфайла проекта. Затем мы можем сгенерировать базовый README в формате Markdown и записать его в README.mdфайл:

const { name: projectName } = await readPackageJson (targetDirectory) ;

const readmeContents = `# ${projectName}

...

`;

await fs.writeFile (`${targetDirectory}/README.md`, readmeContents) ;

В приведенной выше функции мы используем вариант обещания fs.writeFile, предоставленный fs-extra.

Зафиксировать скелет проекта в Git

Наконец, пришло время зафиксировать скелет проекта, который мы создали с помощью git:

await $`git add. `;

await $`git commit -m «Add project skeleton»`;

Затем мы отобразим сообщение, подтверждающее, что наш новый проект успешно загружен:

console.log (

chalk.green (

`\n✔️ The project ${projectName} has been successfully bootstrapped! \n`

)

) ;

console.log (chalk.green (`Add a git remote and push your changes. `));

Загрузить новый проект

Теперь мы можем использовать созданный нами инструмент для начальной загрузки нового проекта:

mkdir new-project

. /bootstrap-tool.mjs —directory new-project

И посмотрите все, что мы собрали, в действии!

Заключение

В этой статье мы узнали, как создавать мощные сценарии оболочки в Node.js с помощью библиотеки Google zx. Мы использовали служебные функции и библиотеки, которые он предоставляет, для создания гибкого инструмента командной строки.

Инструмент, который мы создали до сих пор, — это только начало. Вот несколько идей, которые вы могли бы попробовать добавить самостоятельно:

Автоматически создать целевой каталог. Если целевой каталог еще не существует, запросите пользователя и спросите, не хотят ли они, чтобы он был создан для него.

Гигиена с открытым исходным кодом. Спросите пользователя, создает ли он проект с открытым исходным кодом. Если это так, запустите команды для создания файлов лицензий и Contributor Convenant.

Автоматизируйте создание репозитория на GitHub. Добавьте команды, которые используют интерфейс командной строки GitHub для создания удаленного репозитория на GitHub. Затем новый проект можно отправить в этот репозиторий после того, как исходный скелет будет зафиксирован с помощью Git.

Делитесь нашими материалами с друзьями!

 

 

Заказать разработку сайта