avaScript — самый популярный скриптовый язык в мире.
Так почему же так сложно запускать shell-скрипты на JavaScript?
import { spawnSync } from "child_process";
// this is a lot more work than it could be
const { status, stdout, stderr } = spawnSync("ls", ["-l", "*.js"], {
encoding: "utf8",
});
Также можно использовать Node.js API, чтобы сделать что-то подобное:
import { readdir } from "fs/promises";
(await readdir(".", { withFileTypes: true })).filter((a) =>
a.name.endsWith(".js")
);
Но это все еще не так просто, как shell:
ls *.js
macOS (zsh), Linux (bash) и Windows (cmd) имеют немного разные оболочки с разным синтаксисом и разными командами. Команды, доступные на каждой платформе, разные, и даже одна и та же команда может иметь разные флаги и поведение.
На сегодняшний день решение npm заключается в том, чтобы положиться на сообщество, которое дополнит недостающие команды реализациями JavaScript.
На x64 Hetzner Arch Linux это занимает около 7 мс:
$ hyperfine --warmup 3 'bash -c "echo hello"' 'sh -c "echo hello"' -N
Benchmark 1: bash -c 'echo hello'
Time (mean ± σ): 7.3 ms ± 1.5 ms [User: 5.1 ms, System: 1.9 ms]
Range (min … max): 1.7 ms … 9.4 ms 529 runs
Benchmark 2: sh -c 'echo hello'
Time (mean ± σ): 7.2 ms ± 1.6 ms [User: 4.8 ms, System: 2.1 ms]
Range (min … max): 1.5 ms … 9.6 ms 327 runs
Если вы намерены запустить одну команду, запуск оболочки может занять больше времени, чем время работы самой команды. Если вы выполняете много команд в цикле, это быстро становится дорогостоящим.
Вы можете попробовать встроить shell, но это действительно сложно, и их лицензия может быть несовместима с вашим проектом.
Мы можем лучше!
Bun Shell — это новый экспериментальный встроенный язык и интерпретатор внутри Bun, который позволяет запускать кроссплатформенные сценарии оболочки на JavaScript и TypeScript.
import { $ } from "bun";
// to stdout:
await $`ls *.js`;
// to string:
const text = await $`ls *.js`.text();
Вы можете использовать переменные JavaScript в своих shell-скриптах:
import { $ } from "bun";
const resp = await fetch("https://example.com");
const stdout = await $`gzip -c < ${resp}`.arrayBuffer();
В целях безопасности все переменные шаблона экранированы:
const filename = "foo.js; rm -rf /";
// This will run `ls 'foo.js; rm -rf /'`
const results = await $`ls ${filename}`;
console.log(results.exitCode); // 1
console.log(results.stderr.toString()); // ls: cannot access 'foo.js; rm -rf /': No such file or directory
Использование Bun Shell похоже на обычный JavaScript. Вы можете перенаправить (с помощью >) стандартный вывод в буферы:
import { $ } from "bun";
const buffer = Buffer.alloc(1024);
await $`ls *.js > ${buffer}`;
console.log(buffer.toString("utf8"));
Можно перенаправить стандартный вывод в файл:
import { $, file } from "bun";
// as a file()
await $`ls *.js > ${file("output.txt")}`;
// or as a file path string, if you prefer:
await $`ls *.js > output.txt`;
await $`ls *.js > ${"output.txt"}`;
С помощью pipe (знак |) можно передать стандартный вывод другой команде:
import { $ } from "bun";
await $`ls *.js | grep foo`;
Можно использовать Response в качестве стандартного ввода:
import { $ } from "bun";
const buffer = new Response("bar\n foo\n bar\n foo\n");
await $`grep foo < ${buffer}`;
Доступны встроенные команды, такие как cd, echoи rm:
import { $ } from "bun";
await $`cd .. && rm -rf node_modules/rimraf`;
Это работает в Windows, macOS и Linux. Мы реализовали множество общих команд и возможностей, такие как подстановка (globbing), переменные окружения, перенаправление, пайпы и многое другое.
Bun Shell разработан как замена простым shell-скриптам. В Windows он будет интерпретировать scripts из package.json при запуске через bun run.
По приколу вы также можете использовать его как отдельный интерпретатор сценариев оболочки:
echo "cat package.json" > script.bun.sh
bun script.bun.sh
bun --version
1.0.24
Если у вас не установлен Bun, вы можете установить его с помощью curl:
curl -fsSL <https://bun.sh/install> | bash
Или с помощью npm:
npm install -g bun
Bun Shell находится в статусе Alpha, соответственно завышать ожидания не стоит.
Вот тут - https://bun.sh/docs/runtime/shell - находится официальная документация. При проектировании этой истории вдохновлялись проектами zx, dax, and bnx.
Для себя главную ценность вижу именно в выразительности shell cинтаксиса, в сравнении с await readdir(...) и подобным, и одновременно возможности использовать привычный язык для описания самой логики.
Выглядит это все, конечно, занятно/интересно/многообещающе. Но лично я перестал сильно обращать внимание на проблему взаимодействия Node.js и Shell, и вот видимо почему:
Так почему же так сложно запускать shell-скрипты на JavaScript?
import { spawnSync } from "child_process";
// this is a lot more work than it could be
const { status, stdout, stderr } = spawnSync("ls", ["-l", "*.js"], {
encoding: "utf8",
});
Также можно использовать Node.js API, чтобы сделать что-то подобное:
import { readdir } from "fs/promises";
(await readdir(".", { withFileTypes: true })).filter((a) =>
a.name.endsWith(".js")
);
Но это все еще не так просто, как shell:
ls *.js
Почему существующие shell-оболочки не работают в JavaScript
bashили shсуществуют уже несколько десятилетий.Но они плохо работают в JavaScript. Почему?Shell - это решенная проблема!!
Комментатор Hacker New, возможно.
macOS (zsh), Linux (bash) и Windows (cmd) имеют немного разные оболочки с разным синтаксисом и разными командами. Команды, доступные на каждой платформе, разные, и даже одна и та же команда может иметь разные флаги и поведение.
На сегодняшний день решение npm заключается в том, чтобы положиться на сообщество, которое дополнит недостающие команды реализациями JavaScript.
rm -rf не работает в Windows
rimraf - кроссплатформенная реализация JavaScript rm -rf - загружается более 60 миллионов раз в неделю:Переменные среды, например, FOO=bar <script> не работают в Windows
Установка переменных среды различна на каждой платформе. Вместо простой надписи FOO=barвы, вероятно, устанавливаете и используете cross-env:which - это where на Windows
Таким образом родился еще один пакет с 60 миллионами загрузок в неделю:Оболочки командной строки слишком долго запускаются
Сколько времени занимает запуск shell?На x64 Hetzner Arch Linux это занимает около 7 мс:
$ hyperfine --warmup 3 'bash -c "echo hello"' 'sh -c "echo hello"' -N
Benchmark 1: bash -c 'echo hello'
Time (mean ± σ): 7.3 ms ± 1.5 ms [User: 5.1 ms, System: 1.9 ms]
Range (min … max): 1.7 ms … 9.4 ms 529 runs
Benchmark 2: sh -c 'echo hello'
Time (mean ± σ): 7.2 ms ± 1.6 ms [User: 4.8 ms, System: 2.1 ms]
Range (min … max): 1.5 ms … 9.6 ms 327 runs
Если вы намерены запустить одну команду, запуск оболочки может занять больше времени, чем время работы самой команды. Если вы выполняете много команд в цикле, это быстро становится дорогостоящим.
Вы можете попробовать встроить shell, но это действительно сложно, и их лицензия может быть несовместима с вашим проектом.
Действительно ли необходимы все эти полифилы?
В 2009–2016 годах, когда JavaScript был еще относительно новым и экспериментальным, полагаться на сообщество для заполнения недостающих функций имело большой смысл. Но сейчас 2024 год. JavaScript на сервере является зрелым и широко распространенным. Экосистема JavaScript понимает сегодняшние требования так, как никто не понимал в 2009 году.Мы можем лучше!
Представляем Bun Shell
Примечание переводчика: Если хочется немного больше прочесть непосредственно про сам Bun, то предлагаю к прочтению анонс - Релиз Bun 1.0 (новый runtime для JavaScript).Bun Shell — это новый экспериментальный встроенный язык и интерпретатор внутри Bun, который позволяет запускать кроссплатформенные сценарии оболочки на JavaScript и TypeScript.
import { $ } from "bun";
// to stdout:
await $`ls *.js`;
// to string:
const text = await $`ls *.js`.text();
Вы можете использовать переменные JavaScript в своих shell-скриптах:
import { $ } from "bun";
const resp = await fetch("https://example.com");
const stdout = await $`gzip -c < ${resp}`.arrayBuffer();
В целях безопасности все переменные шаблона экранированы:
const filename = "foo.js; rm -rf /";
// This will run `ls 'foo.js; rm -rf /'`
const results = await $`ls ${filename}`;
console.log(results.exitCode); // 1
console.log(results.stderr.toString()); // ls: cannot access 'foo.js; rm -rf /': No such file or directory
Использование Bun Shell похоже на обычный JavaScript. Вы можете перенаправить (с помощью >) стандартный вывод в буферы:
import { $ } from "bun";
const buffer = Buffer.alloc(1024);
await $`ls *.js > ${buffer}`;
console.log(buffer.toString("utf8"));
Можно перенаправить стандартный вывод в файл:
import { $, file } from "bun";
// as a file()
await $`ls *.js > ${file("output.txt")}`;
// or as a file path string, if you prefer:
await $`ls *.js > output.txt`;
await $`ls *.js > ${"output.txt"}`;
С помощью pipe (знак |) можно передать стандартный вывод другой команде:
import { $ } from "bun";
await $`ls *.js | grep foo`;
Можно использовать Response в качестве стандартного ввода:
import { $ } from "bun";
const buffer = new Response("bar\n foo\n bar\n foo\n");
await $`grep foo < ${buffer}`;
Доступны встроенные команды, такие как cd, echoи rm:
import { $ } from "bun";
await $`cd .. && rm -rf node_modules/rimraf`;
Это работает в Windows, macOS и Linux. Мы реализовали множество общих команд и возможностей, такие как подстановка (globbing), переменные окружения, перенаправление, пайпы и многое другое.
Bun Shell разработан как замена простым shell-скриптам. В Windows он будет интерпретировать scripts из package.json при запуске через bun run.
По приколу вы также можете использовать его как отдельный интерпретатор сценариев оболочки:
echo "cat package.json" > script.bun.sh
bun script.bun.sh
Как установить?
Bun Shell встроен в Bun . Если у вас уже установлен Bun v1.0.24 или новее, то вы уже можете его использовать:bun --version
1.0.24
Если у вас не установлен Bun, вы можете установить его с помощью curl:
curl -fsSL <https://bun.sh/install> | bash
Или с помощью npm:
npm install -g bun
Bun Shell находится в статусе Alpha, соответственно завышать ожидания не стоит.
Вот тут - https://bun.sh/docs/runtime/shell - находится официальная документация. При проектировании этой истории вдохновлялись проектами zx, dax, and bnx.
Для себя главную ценность вижу именно в выразительности shell cинтаксиса, в сравнении с await readdir(...) и подобным, и одновременно возможности использовать привычный язык для описания самой логики.
Выглядит это все, конечно, занятно/интересно/многообещающе. Но лично я перестал сильно обращать внимание на проблему взаимодействия Node.js и Shell, и вот видимо почему:
- Давно не пересекаюсь с Windows и коллеги-разработчики используют либо MacOS, либо Linux. Давно не ставил cross-env из-за жесткой необходимости.
- Современные сборщики для Frontend уже давно умеют самостоятельно очищать dist-директорию без rimraf (Webpack и Vite точно умеют).
Даже загрузку статики на S3 (для дальнейшей раздачи через CDN) можно делать через плагины, а не shell + rclone или еще какой-то "дедовский" способ. - При сборке Docker тоже не помню, чтобы приходилось писать какие-то сложные скрипты. Без нюансов всяких multi stage билдов обычно все ограничивается установкой зависимостей, а затем сборкой TypeScript.
- А вот в рамках CI действительно иной раз хочется что-то такое на стыке шелла и js написать, но пары вызовов node -e или node -p (для вывода результата кода сразу в stdout) опять таки хватает
Релиз Bun Shell (новый shell для JavaScript)
JavaScript — самый популярный скриптовый язык в мире. Так почему же так сложно запускать shell-скрипты на JavaScript? import { spawnSync } from "child_process"; // this is a lot more work than it...
habr.com