Skip to content

Bun Shell hace que el scripting de shell con JavaScript y TypeScript sea divertido. Es un shell multiplataforma similar a bash con interoperabilidad perfecta con JavaScript.

Inicio rápido:

ts
import { $ } from "bun";

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

// Usa Response como stdin.
await $`cat < ${response} | wc -c`; // 1256

Características

  • Multiplataforma: funciona en Windows, Linux y macOS. En lugar de rimraf o cross-env, puedes usar Bun Shell sin instalar dependencias adicionales. Comandos de shell comunes como ls, cd, rm están implementados nativamente.
  • Familiar: Bun Shell es un shell similar a bash, soportando redirección, tuberías, variables de entorno y más.
  • Globs: Los patrones Glob se soportan nativamente, incluyendo **, *, {expansion}, y más.
  • Template literals: Los template literals se usan para ejecutar comandos de shell. Esto permite una interpolación fácil de variables y expresiones.
  • Seguridad: Bun Shell escapa todas las cadenas por defecto, previniendo ataques de inyección de shell.
  • Interoperabilidad con JavaScript: Usa Response, ArrayBuffer, Blob, Bun.file(path) y otros objetos de JavaScript como stdin, stdout y stderr.
  • Scripting de shell: Bun Shell se puede usar para ejecutar scripts de shell (archivos .bun.sh).
  • Intérprete personalizado: Bun Shell está escrito en Zig, junto con su lexer, parser e intérprete. Bun Shell es un pequeño lenguaje de programación.

Primeros Pasos

El comando de shell más simple es echo. Para ejecutarlo, usa el template literal tag $:

js
import { $ } from "bun";

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

Por defecto, los comandos de shell imprimen a stdout. Para silenciar la salida, llama a .quiet():

js
import { $ } from "bun";

await $`echo "Hello World!"`.quiet(); // Sin salida

¿Qué pasa si quieres acceder a la salida del comando como texto? Usa .text():

js
import { $ } from "bun";

// .text() automáticamente llama a .quiet() por ti
const welcome = await $`echo "Hello World!"`.text();

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

Por defecto, hacer await devolverá stdout y stderr como Buffers.

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) []

Manejo de Errores

Por defecto, los códigos de salida distintos de cero lanzarán un error. Este ShellError contiene información sobre el comando ejecutado.

js
import { $ } from "bun";

try {
  const output = await $`something-that-may-fail`.text();
  console.log(output);
} catch (err) {
  console.log(`Falló con código ${err.exitCode}`);
  console.log(err.stdout.toString());
  console.log(err.stderr.toString());
}

El lanzamiento se puede deshabilitar con .nothrow(). El exitCode del resultado necesitará verificarse manualmente.

js
import { $ } from "bun";

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

if (exitCode !== 0) {
  console.log(`Código de salida distinto de cero ${exitCode}`);
}

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

El manejo predeterminado de códigos de salida distintos de cero se puede configurar llamando a .nothrow() o .throws(boolean) en la función $ misma.

js
import { $ } from "bun";
// las promesas de shell no lanzarán, lo que significa que tendrás que
// verificar `exitCode` manualmente en cada comando de shell.
$.nothrow(); // equivalente a $.throws(false)

// comportamiento predeterminado, los códigos de salida distintos de cero lanzarán un error
$.throws(true);

// alias para $.nothrow()
$.throws(false);

await $`something-that-may-fail`; // No se lanza ninguna excepción

Redirección

La entrada o salida de un comando puede ser redirigida usando los operadores típicos de Bash:

  • < redirigir stdin
  • > o 1> redirigir stdout
  • 2> redirigir stderr
  • &> redirigir tanto stdout como stderr
  • >> o 1>> redirigir stdout, añadiendo al destino, en lugar de sobrescribir
  • 2>> redirigir stderr, añadiendo al destino, en lugar de sobrescribir
  • &>> redirigir tanto stdout como stderr, añadiendo al destino, en lugar de sobrescribir
  • 1>&2 redirigir stdout a stderr (todas las escrituras a stdout irán a stderr)
  • 2>&1 redirigir stderr a stdout (todas las escrituras a stderr irán a stdout)

Bun Shell también soporta redirigir desde y hacia objetos de JavaScript.

Ejemplo: Redirigir salida a objetos de JavaScript (>)

Para redirigir stdout a un objeto de JavaScript, usa el operador >:

js
import { $ } from "bun";

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

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

Los siguientes objetos de JavaScript se soportan para redirección a:

  • Buffer, Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, ArrayBuffer, SharedArrayBuffer (escribe en el buffer subyacente)
  • Bun.file(path), Bun.file(fd) (escribe en el archivo)

Ejemplo: Redirigir entrada desde objetos de JavaScript (<)

Para redirigir la salida desde objetos de JavaScript a stdin, usa el operador <:

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

Los siguientes objetos de JavaScript se soportan para redirección desde:

  • Buffer, Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, ArrayBuffer, SharedArrayBuffer (lee del buffer subyacente)
  • Bun.file(path), Bun.file(fd) (lee del archivo)
  • Response (lee del cuerpo)

Ejemplo: Redirigir stdin -> archivo

js
import { $ } from "bun";

await $`cat < myfile.txt`;

Ejemplo: Redirigir stdout -> archivo

js
import { $ } from "bun";

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

Ejemplo: Redirigir stderr -> archivo

js
import { $ } from "bun";

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

Ejemplo: Redirigir stderr -> stdout

js
import { $ } from "bun";

// redirige stderr a stdout, así que toda la salida
// estará disponible en stdout
await $`bun run ./index.ts 2>&1`;

Ejemplo: Redirigir stdout -> stderr

js
import { $ } from "bun";

// redirige stdout a stderr, así que toda la salida
// estará disponible en stderr
await $`bun run ./index.ts 1>&2`;

Tuberías (|)

Como en bash, puedes tuberizar la salida de un comando a otro:

js
import { $ } from "bun";

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

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

También puedes tuberizar con objetos de 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

Sustitución de comandos ($(...))

La sustitución de comandos te permite sustituir la salida de otro script en el script actual:

js
import { $ } from "bun";

// Imprime el hash del commit actual
await $`echo Hash del commit actual: $(git rev-parse HEAD)`;

Esta es una inserción textual de la salida del comando y se puede usar para, por ejemplo, declarar una variable de shell:

js
import { $ } from "bun";

await $`
  REV=$(git rev-parse HEAD)
  docker built -t myapp:$REV
  echo Done building docker image "myapp:$REV"
`;

NOTE

Porque Bun internamente usa la propiedad especial raw en el template literal de entrada, usar la sintaxis de backtick para sustitución de comandos no funcionará:

ts
import { $ } from "bun";

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

En lugar de imprimir:

hi

Lo anterior imprimirá:

echo hi

En su lugar, recomendamos usar la sintaxis $(...).


Variables de entorno

Las variables de entorno se pueden establecer como en bash:

js
import { $ } from "bun";

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

Puedes usar interpolación de cadenas para establecer variables de entorno:

js
import { $ } from "bun";

const foo = "bar123";

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

La entrada se escapa por defecto, previniendo ataques de inyección de shell:

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

Cambiar las variables de entorno

Por defecto, process.env se usa como las variables de entorno para todos los comandos.

Puedes cambiar las variables de entorno para un solo comando llamando a .env():

js
import { $ } from "bun";

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

Puedes cambiar las variables de entorno predeterminadas para todos los comandos llamando a $.env:

js
import { $ } from "bun";

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

// la $FOO establecida globalmente
await $`echo $FOO`; // bar

// la $FOO establecida localmente
await $`echo $FOO`.env({ FOO: "baz" }); // baz

Puedes restablecer las variables de entorno a los valores predeterminados llamando a $.env() sin argumentos:

js
import { $ } from "bun";

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

// la $FOO establecida globalmente
await $`echo $FOO`; // bar

// la $FOO establecida localmente
await $`echo $FOO`.env(undefined); // ""

Cambiar el directorio de trabajo

Puedes cambiar el directorio de trabajo de un comando pasando una cadena a .cwd():

js
import { $ } from "bun";

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

Puedes cambiar el directorio de trabajo predeterminado para todos los comandos llamando a $.cwd:

js
import { $ } from "bun";

$.cwd("/tmp");

// el directorio de trabajo establecido globalmente
await $`pwd`; // /tmp

// el directorio de trabajo establecido localmente
await $`pwd`.cwd("/"); // /

Leer salida

Para leer la salida de un comando como una cadena, usa .text():

js
import { $ } from "bun";

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

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

Leer salida como JSON

Para leer la salida de un comando como JSON, usa .json():

js
import { $ } from "bun";

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

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

Leer salida línea por línea

Para leer la salida de un comando línea por línea, usa .lines():

js
import { $ } from "bun";

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

También puedes usar .lines() en un comando completado:

js
import { $ } from "bun";

const search = "bun";

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

Leer salida como Blob

Para leer la salida de un comando como Blob, usa .blob():

js
import { $ } from "bun";

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

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

Comandos Integrados

Para compatibilidad multiplataforma, Bun Shell implementa un conjunto de comandos integrados, además de leer comandos desde la variable de entorno PATH.

  • cd: cambiar el directorio de trabajo
  • ls: listar archivos en un directorio
  • rm: eliminar archivos y directorios
  • echo: imprimir texto
  • pwd: imprimir el directorio de trabajo
  • bun: ejecutar bun en bun
  • cat
  • touch
  • mkdir
  • which
  • mv
  • exit
  • true
  • false
  • yes
  • seq
  • dirname
  • basename

Parcialmente implementado:

  • mv: mover archivos y directorios (falta soporte multi-dispositivo)

No implementado aún, pero planeado:


Utilidades

Bun Shell también implementa un conjunto de utilidades para trabajar con shells.

$.braces (expansión de llaves)

Esta función implementa expansión de llaves simple para comandos de shell:

js
import { $ } from "bun";

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

$.escape (escapar cadenas)

Expone la lógica de escape de Bun Shell como una función:

js
import { $ } from "bun";

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

Si no quieres que tu cadena sea escapada, envuélvela en un objeto { raw: 'str' }:

js
import { $ } from "bun";

await $`echo ${{ raw: '$(foo) `bar` "baz"' }}`;
// => bun: command not found: foo
// => bun: command not found: bar
// => baz

Cargador de archivos .sh

Para scripts de shell simples, en lugar de /bin/sh, puedes usar Bun Shell para ejecutar scripts de shell.

Para hacerlo, simplemente ejecuta el script con bun en un archivo con la extensión .sh.

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

Los scripts con Bun Shell son multiplataforma, lo que significa que funcionan en Windows:

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

Notas de implementación

Bun Shell es un pequeño lenguaje de programación en Bun que está implementado en Zig. Incluye un lexer, parser e intérprete escritos a mano. A diferencia de bash, zsh y otros shells, Bun Shell ejecuta operaciones concurrentemente.


Seguridad en el shell de Bun

Por diseño, el shell de Bun no invoca un shell del sistema (como /bin/sh) y es en cambio una reimplementación de bash que se ejecuta en el mismo proceso de Bun, diseñado con la seguridad en mente.

Al analizar argumentos de comando, trata todas las variables interpoladas como cadenas únicas y literales.

Esto protege al shell de Bun contra inyección de comandos:

js
import { $ } from "bun";

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

// SEGURO: `userInput` se trata como una cadena única entre comillas
await $`ls ${userInput}`;

En el ejemplo anterior, userInput se trata como una cadena única. Esto hace que el comando ls intente leer el contenido de un único directorio llamado "my-file; rm -rf /".

Consideraciones de seguridad

Aunque la inyección de comandos se previene por defecto, los desarrolladores siguen siendo responsables de la seguridad en ciertos escenarios.

Similar a las APIs Bun.spawn o node:child_process.exec(), puedes intencionalmente ejecutar un comando que genera un nuevo shell (ej. bash -c) con argumentos.

Cuando haces esto, entregas el control, y las protecciones incorporadas de Bun ya no se aplican a la cadena interpretada por ese nuevo shell.

js
import { $ } from "bun";

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

// INSEGURO: Has iniciado explícitamente un nuevo proceso de shell con `bash -c`.
// Este nuevo shell ejecutará el comando `touch`. Cualquier entrada de usuario
// pasada de esta manera debe ser rigurosamente sanitizada.
await $`bash -c "echo ${userInput}"`;

Inyección de argumentos

El shell de Bun no puede saber cómo un comando externo interpreta sus propios argumentos de línea de comandos. Un atacante puede proporcionar entrada que el programa objetivo reconoce como una de sus propias opciones o banderas, llevando a comportamiento no intencionado.

js
import { $ } from "bun";

// Entrada maliciosa formateada como un flag de línea de comandos de Git
const branch = "--upload-pack=echo pwned";

// INSEGURO: Aunque Bun pasa la cadena de forma segura como un único argumento,
// el propio programa `git` ve y actúa sobre el flag malicioso.
await $`git ls-remote origin ${branch}`;

NOTE

**Recomendación** — Como es la mejor práctica en todos los lenguajes, siempre sanitiza la entrada proporcionada por el usuario antes de pasarla como un argumento a un comando externo. La responsabilidad de validar argumentos recae en el código de tu aplicación.

Créditos

Grandes partes de esta API fueron inspiradas por zx, dax, y bnx. Gracias a los autores de esos proyectos.

Bun por www.bunjs.com.cn editar