En esta página

Bash, Zsh y Fish

Los tres son shells: programas que leen tus comandos, los interpretan y piden al sistema que los ejecute. La diferencia está en cuánto te ayudan, qué sintaxis aceptan y qué tan lejos están del estándar.

La tabla que resume todo

BashZshFish
Nombre completoBourne Again SHellZ shellFriendly Interactive SHell
Dónde viene por defectocasi todo LinuxmacOSalgunos Linux
Compatible con POSIXcasicasino
Autocompletadobásico (mejorable)muy buenoexcelente, de fábrica
Sugerencias mientras escribesnocon pluginssí, de fábrica
Resaltado de sintaxisnocon pluginssí, de fábrica
Sintaxis de scriptsestándarestándar + extraspropia
Configuración~/.bashrc~/.zshrc~/.config/fish/config.fish
Curva de aprendizajemediamediabaja

La recomendación práctica: aprende bash para scripting (es lo que vas a encontrar en servidores, CI y ejercicios), usa zsh o fish para el día a día si te gustan, y escribe los scripts siempre en bash o sh.

Qué es POSIX y cómo se estructura un script

POSIX no es un programa ni un shell: es un conjunto de normas que dice cómo debe comportarse un sistema operativo tipo Unix. Para lo que nos importa, define:

  • qué comandos tienen que existir (cd, echo, grep, sed, test…);
  • cómo se comporta el shell;
  • qué sintaxis tiene el lenguaje de scripting;
  • cómo se pasan variables de entorno;
  • qué significa el código de salida de un comando.

Si un script dice #!/bin/sh y usa solo lo que POSIX define, tiene que correr igual en Linux, macOS, BSD o un contenedor mínimo. Eso es lo que lo hace portable.

La estructura de un script POSIX

Un script es un archivo de texto común. Se lee de arriba a abajo, como una receta. El orden habitual es:

1. shebang          → con qué programa ejecutarlo
2. comentarios      → qué hace y cómo se usa
3. configuración    → opciones del shell
4. variables        → datos que vas a reutilizar
5. funciones        → bloques de comandos con nombre
6. programa         → lo que se ejecuta de verdad

Ejemplo mínimo, con cada parte señalada:

#!/bin/sh
# compilar.sh: compila un archivo de C y avisa si falta algo.

set -e

salida="programa"
fuente="$1"

compilar() {
    cc -Wall -Wextra -Werror "$1" -o "$salida"
}

if [ -z "$fuente" ]; then
    echo "Uso: $0 archivo.c" >&2
    exit 1
fi

compilar "$fuente"
echo "Listo: $salida"
LíneaQué hace
#!/bin/shel shebang: indica que lo ejecute sh. Va en la primera línea, sin espacios antes.
# ...comentario: el shell lo ignora. Sirve para explicar el por qué.
set -esi un comando falla, el script corta. Evita seguir con datos rotos.
salida="programa"variable. No lleva espacios alrededor del =.
fuente="$1"$1 es el primer argumento que recibe el script.
compilar() { ... }función: un bloque de comandos con nombre.
[ -z "$fuente" ]prueba POSIX: verdadera si la cadena está vacía.
>&2manda el mensaje al canal de errores.
exit 1termina con código de error (0 significa "salió bien").

La gramática de una línea

Casi todo comando sigue esta forma:

comando [opciones] [argumentos] [redirecciones]
grep   -rn   "TODO"   src/   > pendientes.txt
│      │     │        │      │
│      │     │        │      └─ redirección: guarda la salida en un archivo
│      │     │        └──────── argumento: dónde buscar
│      │     └───────────────── argumento: qué buscar
│      └─────────────────────── opciones: modifican el comportamiento
└────────────────────────────── comando: el programa que se ejecuta

Los operadores que combinan comandos son parte del lenguaje:

OperadorQué haceEjemplo
|conecta la salida de uno con la entrada del otrols | wc -l
>guarda la salida en un archivo (lo pisa)echo hola > saludo.txt
>>agrega al final del archivoecho otra >> saludo.txt
<toma la entrada desde un archivowc -l < lista.txt
2>redirige solo los errorescmd 2> errores.txt
;ejecuta uno y después el otro, pase lo que pasecd /tmp; ls
&&ejecuta el segundo solo si el primero salió biengcc main.c && ./a.out
||ejecuta el segundo solo si el primero fallócd carpeta || exit 1

Las piezas del lenguaje

PiezaCómo se escribePara qué
variablenombre="valor"guardar un dato
usar variable"$nombre"leerlo (siempre entre comillas)
argumento$1, $2, $#lo que recibe el script
condiciónif [ ... ]; then ... fidecidir
buclefor x in ...; do ... donerepetir
bucle condicionalwhile [ ... ]; do ... donerepetir mientras
funciónnombre() { ... }agrupar comandos y reutilizarlos
código de salida$?, exit 0, exit 1decir si salió bien o mal

Qué es una función (y por qué aparece en todos lados)

Una función es un bloque de comandos con un nombre. En vez de repetir las mismas cinco líneas en tres lugares, las escribes una vez y las llamas por su nombre. Eso es todo.

saludar() {
    echo "Hola, $1"
}

saludar "Ana"     # imprime: Hola, Ana
saludar "Luis"    # imprime: Hola, Luis
  • saludar es el nombre.
  • $1 es el primer argumento que recibe la función.
  • El cuerpo va entre { y }.
  • local nombre="$1" (en bash) hace que la variable no se escape de la función.

La misma idea existe en C, en Python y en casi todos los lenguajes: un bloque de código con nombre que puedes ejecutar cuando quieras. En los scripts sirve para no repetir y para que el script se lea como una lista de pasos.

POSIX vs bash

Shell¿Es POSIX?Consecuencia
sh (dash en Debian/Ubuntu)símínimo común denominador
bashcasi (tiene extensiones)seguro en casi todos lados
zshcasiacepta lo de bash, con variantes
fishnoun script de fish no corre en bash, ni al revés

Diferencias que te van a morder:

QuieresPOSIX (sh)bash / zsh
comparar cadenas[ "$a" = "$b" ][[ "$a" == "$b" ]]
condición combinada[ -f x ] && [ -r x ][[ -f x && -r x ]]
arraysno tienefrutas=(manzana pera)
$(...)sísí
localno estándarsí
pipefailnoset -o pipefail

Regla: si el script va a correr en otro lado, #!/bin/sh con sintaxis POSIX, o #!/usr/bin/env bash si necesitas las extensiones de bash. Fish es para tu terminal, no para scripts portables.

Bash

El que vas a encontrar en el 95 % de los servidores Linux.

echo "$BASH_VERSION"
ArchivoCuándo se carga
~/.bashrccada terminal interactiva (lo más común)
~/.bash_profileal iniciar sesión
~/.profilelogin, si no existe .bash_profile

Cosas que sabe hacer:

frutas=("manzana" "pera")       # arrays, índice 0
echo "${frutas[0]}"             # manzana
[[ -f archivo && -r archivo ]]  # condiciones combinadas
for f in *.c; do echo "$f"; done

Es el shell de referencia para scripting: todo lo del documento de terminal aplica aquí.

Zsh

Nació como una mejora de bash. Es el shell por defecto de macOS desde 2019.

echo "$ZSH_VERSION"
ArchivoCuándo se carga
~/.zshenvsiempre
~/.zshrccada terminal interactiva
~/.zprofileal iniciar sesión

Lo que gana sobre bash:

FunciónEjemplo
Autocompletado potenteTab cicla opciones con descripción
Globbing extendidols **/*.c recursivo
Corrección de typossetopt correct
Temas y pluginsoh-my-zsh, zinit, antidote
Prompt a medidapowerlevel10k, starship

Detalles que sorprenden:

  • Los arrays empiezan en 1, no en 0: ${array[1]} es el primero.
  • **/ recursivo viene activado.
  • Con setopt cambias comportamientos que en bash son fijos.

Instalación típica (Ubuntu/Debian):

sudo apt install zsh
chsh -s "$(which zsh)"

Cierra sesión y vuelve a entrar. Para probar sin cambiar nada: zsh.

Fish

Está diseñado para ser cómodo sin configurar nada.

echo $FISH_VERSION
fish_config    # abre la configuración en el navegador

Lo que trae de fábrica:

FunciónCómo se ve
Sugerenciasgris claro con el resto del comando anterior
Resaltadoverde si el comando existe, rojo si no
Autocompletadocon descripciones y pestañas
Historial compartidoentre terminales, al instante
Funciones con nombrefunction saludo ... end

El costo: sintaxis propia. No es POSIX.

QuieresBash / ZshFish
asignar variableVAR="hola"set VAR hola
exportarexport VAR=holaset -gx VAR hola
capturar salida$(comando)(comando)
si falló$?$status
Y lógico&&and (o && moderno)
funciónf() { ... }function f; ...; end
condición[[ -f x ]]test -f x

Por eso: fish para la terminal, bash para los scripts.

Cómo cambiar de shell

cat /etc/shells              # shells permitidos en el sistema
chsh -s /usr/bin/zsh         # cambia tu shell por defecto
exec zsh                     # prueba sin cerrar la sesión

Si chsh dice que el shell no está en /etc/shells, hay que agregarlo (con permisos de administrador) o instalar el paquete que lo provee.

Configuración que sí vale la pena

Sin importar el shell, tres cosas mejoran el día a día:

1. Alias

alias ll="ls -la"
alias g="git"
alias gs="git status"
alias ..="cd .."

2. Prompt informativo

Un buen prompt te dice en qué carpeta estás, si hay cambios sin commitear y qué rama tienes. starship funciona en los tres shells:

# ~/.bashrc o ~/.zshrc
eval "$(starship init bash)"   # o zsh

3. Historial compartido

# bash: ~/.bashrc
shopt -s histappend
export HISTSIZE=10000
export HISTFILESIZE=20000

Errores frecuentes

SíntomaCausaSolución
Edité .zshrc y no pasa nadano recargastesource ~/.zshrc
command not found después de instalarel PATH no incluye la carpetaagregarlo al archivo de tu shell
El script anda en bash y no en fishsintaxis incompatibleejecutarlo con bash script.sh
#!/bin/sh falla con [[ ]]sh no soporta esa sintaxisusar #!/usr/bin/env bash
El prompt tarda segundosplugin pesadodesactivar plugins y medir
PATH duplicadoel archivo se carga dos vecesagregar guardas o revisar .profile
permission denied al cambiar shellel shell no está en /etc/shellsagregarlo o instalarlo

Qué elegir

SituaciónElección
Vas a escribir scripts para servidores o proyectosbash
Quieres comodidad sin configurarfish
Quieres compatibilidad + pluginszsh
No quieres pensar en nadael que venga instalado

No hay una respuesta correcta: hay contexto. Lo que sí es universal es saber bash, porque es el que está en la máquina cuando algo se rompe.

Siguiente parada: Vim.


Hecho con amor para Laura, por Bogdan.