Skip to content

Bun Shell делает скрипты оболочки с JavaScript и TypeScript увлекательными. Это кроссплатформенная оболочка, подобная bash, с бесшовной интеграцией JavaScript.

Быстрый старт:

ts
import { $ } from "bun";

const response = await fetch("https://example.com");

// Используйте Response как stdin.
await $`cat < ${response} | wc -c`; // 1256

Возможности

  • Кроссплатформенность: работает на Windows, Linux и macOS. Вместо rimraf или cross-env вы можете использовать Bun Shell без установки дополнительных зависимостей. Общие команды оболочки, такие как ls, cd, rm, реализованы нативно.
  • Знакомая: Bun Shell — это оболочка, подобная bash, поддерживающая перенаправление, конвейеры, переменные окружения и многое другое.
  • Глобы: Глоб-паттерны поддерживаются нативно, включая **, *, {expansion} и другие.
  • Шаблонные литералы: Шаблонные литералы используются для выполнения команд оболочки. Это позволяет легко интерполировать переменные и выражения.
  • Безопасность: Bun Shell экранирует все строки по умолчанию, предотвращая атаки внедрения команд оболочки.
  • Интеграция с JavaScript: Используйте Response, ArrayBuffer, Blob, Bun.file(path) и другие объекты JavaScript как stdin, stdout и stderr.
  • Скрипты оболочки: Bun Shell можно использовать для запуска скриптов оболочки (файлы .bun.sh).
  • Пользовательский интерпретатор: Bun Shell написан на Zig вместе с его лексером, парсером и интерпретатором. Bun Shell — это небольшой язык программирования.

Начало работы

Простейшая команда оболочки — echo. Чтобы выполнить её, используйте шаблонный литерал $:

js
import { $ } from "bun";

await $`echo "Hello World!"`; // Hello World!

По умолчанию команды оболочки выводят в stdout. Чтобы отключить вывод, вызовите .quiet():

js
import { $ } from "bun";

await $`echo "Hello World!"`.quiet(); // Нет вывода

Что если вы хотите получить доступ к выводу команды как к тексту? Используйте .text():

js
import { $ } from "bun";

// .text() автоматически вызывает .quiet() для вас
const welcome = await $`echo "Hello World!"`.text();

console.log(welcome); // Hello World!\n

По умолчанию await возвращает stdout и stderr как Buffer.

js
import { $ } from "bun";

const { stdout, stderr } = await $`echo "Hello!"`.quiet();

console.log(stdout); // Buffer(7) [ 72, 101, 108, 108, 111, 33, 10 ]
console.log(stderr); // Buffer(0) []

Обработка ошибок

По умолчанию коды выхода, отличные от нуля, выбрасывают ошибку. Эта ShellError содержит информацию о выполненной команде.

js
import { $ } from "bun";

try {
  const output = await $`something-that-may-fail`.text();
  console.log(output);
} catch (err) {
  console.log(`Не удалось с кодом ${err.exitCode}`);
  console.log(err.stdout.toString());
  console.log(err.stderr.toString());
}

Выбрасывание можно отключить с помощью .nothrow(). Результат exitCode нужно будет проверять вручную.

js
import { $ } from "bun";

const { stdout, stderr, exitCode } = await $`something-that-may-fail`.nothrow().quiet();

if (exitCode !== 0) {
  console.log(`Код выхода, отличный от нуля: ${exitCode}`);
}

console.log(stdout);
console.log(stderr);

Обработка по умолчанию кодов выхода, отличных от нуля, может быть настроена вызовом .nothrow() или .throws(boolean) на самой функции $.

js
import { $ } from "bun";
// промисы оболочки не будут выбрасывать ошибку, что означает, что вам придется
// проверять `exitCode` вручную для каждой команды оболочки.
$.nothrow(); // эквивалентно $.throws(false)

// поведение по умолчанию, коды выхода, отличные от нуля, будут выбрасывать ошибку
$.throws(true);

// псевдоним для $.nothrow()
$.throws(false);

await $`something-that-may-fail`; // Исключение не выбрасывается

Перенаправление

Вход или выход команды может быть перенаправлен с использованием типичных операторов Bash:

  • < перенаправление stdin
  • > или 1> перенаправление stdout
  • 2> перенаправление stderr
  • &> перенаправление и stdout, и stderr
  • >> или 1>> перенаправление stdout, добавление в назначение вместо перезаписи
  • 2>> перенаправление stderr, добавление в назначение вместо перезаписи
  • &>> перенаправление и stdout, и stderr, добавление в назначение вместо перезаписи
  • 1>&2 перенаправление stdout в stderr (все записи в stdout будут вместо этого в stderr)
  • 2>&1 перенаправление stderr в stdout (все записи в stderr будут вместо этого в stdout)

Bun Shell также поддерживает перенаправление из и в объекты JavaScript.

Пример: Перенаправление вывода в объекты JavaScript (>)

Для перенаправления stdout в объект JavaScript используйте оператор >:

js
import { $ } from "bun";

const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;

console.log(buffer.toString()); // Hello World!\n

Следующие объекты JavaScript поддерживаются для перенаправления в:

  • Buffer, Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, ArrayBuffer, SharedArrayBuffer (запись в базовый буфер)
  • Bun.file(path), Bun.file(fd) (запись в файл)

Пример: Перенаправление ввода из объектов JavaScript (<)

Для перенаправления вывода из объектов JavaScript в stdin используйте оператор <:

js
import { $ } from "bun";

const response = new Response("hello i am a response body");

const result = await $`cat < ${response}`.text();

console.log(result); // hello i am a response body

Следующие объекты JavaScript поддерживаются для перенаправления из:

  • Buffer, Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, ArrayBuffer, SharedArrayBuffer (чтение из базового буфера)
  • Bun.file(path), Bun.file(fd) (чтение из файла)
  • Response (чтение из тела)

Пример: Перенаправление stdin -> файл

js
import { $ } from "bun";

await $`cat < myfile.txt`;

Пример: Перенаправление stdout -> файл

js
import { $ } from "bun";

await $`echo bun! > greeting.txt`;

Пример: Перенаправление stderr -> файл

js
import { $ } from "bun";

await $`bun run index.ts 2> errors.txt`;

Пример: Перенаправление stderr -> stdout

js
import { $ } from "bun";

// перенаправляет stderr в stdout, поэтому весь вывод
// будет доступен в stdout
await $`bun run ./index.ts 2>&1`;

Пример: Перенаправление stdout -> stderr

js
import { $ } from "bun";

// перенаправляет stdout в stderr, поэтому весь вывод
// будет доступен в stderr
await $`bun run ./index.ts 1>&2`;

Конвейеры (|)

Как в bash, вы можете передавать вывод одной команды в другую:

js
import { $ } from "bun";

const result = await $`echo "Hello World!" | wc -w`.text();

console.log(result); // 2\n

Вы также можете использовать конвейеры с объектами JavaScript:

js
import { $ } from "bun";

const response = new Response("hello i am a response body");

const result = await $`cat < ${response} | wc -w`.text();

console.log(result); // 6\n

Подстановка команд ($(...))

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

js
import { $ } from "bun";

// Выводит хэш текущего коммита
await $`echo Хэш текущего коммита: $(git rev-parse HEAD)`;

Это текстовая вставка вывода команды и может использоваться, например, для объявления переменной оболочки:

js
import { $ } from "bun";

await $`
  REV=$(git rev-parse HEAD)
  docker built -t myapp:$REV
  echo Готово создание образа docker "myapp:$REV"
`;

NOTE

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

ts
import { $ } from "bun";

await $`echo \`echo hi\``;

Вместо вывода:

hi

Выше будет выведено:

echo hi

Вместо этого мы рекомендуем придерживаться синтаксиса $(...).


Переменные окружения

Переменные окружения можно установить, как в bash:

js
import { $ } from "bun";

await $`FOO=foo bun -e 'console.log(process.env.FOO)'`; // foo\n

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

js
import { $ } from "bun";

const foo = "bar123";

await $`FOO=${foo + "456"} bun -e 'console.log(process.env.FOO)'`; // bar123456\n

Ввод экранируется по умолчанию, предотвращая атаки внедрения команд оболочки:

js
import { $ } from "bun";

const foo = "bar123; rm -rf /tmp";

await $`FOO=${foo} bun -e 'console.log(process.env.FOO)'`; // bar123; rm -rf /tmp\n

Изменение переменных окружения

По умолчанию process.env используется как переменные окружения для всех команд.

Вы можете изменить переменные окружения для одной команды, вызвав .env():

js
import { $ } from "bun";

await $`echo $FOO`.env({ ...process.env, FOO: "bar" }); // bar

Вы можете изменить переменные окружения по умолчанию для всех команд, вызвав $.env:

js
import { $ } from "bun";

$.env({ FOO: "bar" });

// глобально установленная $FOO
await $`echo $FOO`; // bar

// локально установленная $FOO
await $`echo $FOO`.env({ FOO: "baz" }); // baz

Вы можете сбросить переменные окружения на значения по умолчанию, вызвав $.env() без аргументов:

js
import { $ } from "bun";

$.env({ FOO: "bar" });

// глобально установленная $FOO
await $`echo $FOO`; // bar

// локально установленная $FOO
await $`echo $FOO`.env(undefined); // ""

Изменение рабочего каталога

Вы можете изменить рабочий каталог команды, передав строку в .cwd():

js
import { $ } from "bun";

await $`pwd`.cwd("/tmp"); // /tmp

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

js
import { $ } from "bun";

$.cwd("/tmp");

// глобально установленный рабочий каталог
await $`pwd`; // /tmp

// локально установленный рабочий каталог
await $`pwd`.cwd("/"); // /

Чтение вывода

Для чтения вывода команды как строки используйте .text():

js
import { $ } from "bun";

const result = await $`echo "Hello World!"`.text();

console.log(result); // Hello World!\n

Чтение вывода как JSON

Для чтения вывода команды как JSON используйте .json():

js
import { $ } from "bun";

const result = await $`echo '{"foo": "bar"}'`.json();

console.log(result); // { foo: "bar" }

Чтение вывода построчно

Для чтения вывода команды построчно используйте .lines():

js
import { $ } from "bun";

for await (let line of $`echo "Hello World!"`.lines()) {
  console.log(line); // Hello World!
}

Вы также можете использовать .lines() на завершенной команде:

js
import { $ } from "bun";

const search = "bun";

for await (let line of $`cat list.txt | grep ${search}`.lines()) {
  console.log(line);
}

Чтение вывода как Blob

Для чтения вывода команды как Blob используйте .blob():

js
import { $ } from "bun";

const result = await $`echo "Hello World!"`.blob();

console.log(result); // Blob(13) { size: 13, type: "text/plain" }

Встроенные команды

Для кроссплатформенной совместимости Bun Shell реализует набор встроенных команд в дополнение к чтению команд из переменной окружения PATH.

  • cd: изменить рабочий каталог
  • ls: список файлов в каталоге
  • rm: удаление файлов и каталогов
  • echo: вывод текста
  • pwd: вывод рабочего каталога
  • bun: запуск bun в bun
  • cat
  • touch
  • mkdir
  • which
  • mv
  • exit
  • true
  • false
  • yes
  • seq
  • dirname
  • basename

Частично реализовано:

  • mv: перемещение файлов и каталогов (отсутствует поддержка меж dispositivo)

Еще не реализовано, но запланировано:


Утилиты

Bun Shell также реализует набор утилит для работы с оболочками.

$.braces (расширение фигурных скобок)

Эта функция реализует простое расширение фигурных скобок для команд оболочки:

js
import { $ } from "bun";

await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]

$.escape (экранирование строк)

Предоставляет логику экранирования Bun Shell как функцию:

js
import { $ } from "bun";

console.log($.escape('$(foo) `bar` "baz"'));
// => \$(foo) \`bar\` \"baz\"

Если вы не хотите, чтобы ваша строка экранировалась, оберните её в объект { raw: 'str' }:

js
import { $ } from "bun";

await $`echo ${{ raw: '$(foo) `bar` "baz"' }}`;
// => bun: команда не найдена: foo
// => bun: команда не найдена: bar
// => baz

Загрузчик файлов .sh

Для простых скриптов оболочки вместо /bin/sh вы можете использовать Bun Shell для запуска скриптов оболочки.

Для этого просто запустите скрипт с помощью bun на файле с расширением .sh.

sh
echo "Hello World! pwd=$(pwd)"
sh
bun ./script.sh
txt
Hello World! pwd=/home/demo

Скрипты с Bun Shell кроссплатформенны, что означает, что они работают на Windows:

powershell
bun .\script.sh
txt
Hello World! pwd=C:\Users\Demo

Примечания по реализации

Bun Shell — это небольшой язык программирования в Bun, реализованный на Zig. Он включает рукописный лексер, парсер и интерпретатор. В отличие от bash, zsh и других оболочек, Bun Shell выполняет операции параллельно.


Безопасность в оболочке Bun

По дизайну оболочка Bun не вызывает системную оболочку (как /bin/sh) и вместо этого является переосмыслением bash, которое работает в том же процессе Bun, разработанное с учетом безопасности.

При разборе аргументов команды она рассматривает все интерполированные переменные как одиночные, буквенные строки.

Это защищает оболочку Bun от внедрения команд:

js
import { $ } from "bun";

const userInput = "my-file.txt; rm -rf /";

// БЕЗОПАСНО: `userInput` рассматривается как одна строка в кавычках
await $`ls ${userInput}`;

В приведенном выше примере userInput рассматривается как одна строка. Это заставляет команду ls пытаться прочитать содержимое одного каталога с именем "my-file; rm -rf /".

Соображения безопасности

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

Подобно API Bun.spawn или node:child_process.exec(), вы можете намеренно выполнить команду, которая порождает новую оболочку (например, bash -c) с аргументами.

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

js
import { $ } from "bun";

const userInput = "world; touch /tmp/pwned";

// НЕБЕЗОПАСНО: Вы явно запустили новый процесс оболочки с `bash -c`.
// Эта новая оболочка выполнит команду `touch`. Любой пользовательский ввод,
// переданный таким образом, должен быть тщательно санирован.
await $`bash -c "echo ${userInput}"`;

Внедрение аргументов

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

js
import { $ } from "bun";

// Вредоносный ввод, отформатированный как флаг командной строки Git
const branch = "--upload-pack=echo pwned";

// НЕБЕЗОПАСНО: Хотя Bun безопасно передает строку как один аргумент,
// сама программа `git` видит и действует по вредоносному флагу.
await $`git ls-remote origin ${branch}`;

NOTE

**Рекомендация** — Как это является лучшей практикой в каждом языке, всегда санируйте пользовательский ввод перед передачей его в качестве аргумента внешней команде. Ответственность за валидацию аргументов лежит на коде вашего приложения.

Благодарности

Большие части этого API были вдохновлены zx, dax и bnx. Спасибо авторам этих проектов.

Bun от www.bunjs.com.cn