cv.cm

Programación · Lenguajes de programación

Tutorial de scripting en Bash para principiantes

8 min de lectura

Respuesta corta

Bash es la shell y lenguaje de scripting de línea de comandos de Unix que se usa en servidores Linux, terminales de macOS y pipelines de CI. Un script de Bash es un archivo de texto con comandos de shell, combinados con variables, condiciones, bucles, funciones y tuberías. Esta página muestra cómo escribir y ejecutar scripts, entrecomillar variables de forma segura, controlar el flujo, usar arreglos, redirigir la salida y manejar errores con set -euo pipefail.

Abrir Calculadora chmod

Qué es Bash y para qué se usa

Bash (Bourne Again SHell) es una shell de línea de comandos y lenguaje de scripting del Proyecto GNU. Lee los comandos que escribes, ejecuta programas y los conecta con tuberías y redirecciones. Un script de Bash es un archivo de texto con esos mismos comandos, así que todo lo que haces a mano en una terminal se puede automatizar.

Bash es la shell interactiva predeterminada en la mayoría de las distribuciones de Linux y está disponible en macOS (que ahora usa zsh por defecto, pero sigue incluyendo Bash) y en Windows mediante WSL o Git Bash. Desarrolladores, administradores de sistemas e ingenieros DevOps lo usan para scripts de compilación, despliegue, tareas de CI, mantenimiento de servidores y procesamiento rápido de datos con herramientas como grep, sed, awk y sort.

Escribe y ejecuta Hola, mundo

Un script empieza con una línea shebang que indica al sistema qué intérprete usar. Haz el archivo ejecutable con chmod y luego ejecútalo.

#!/usr/bin/env bash
# hello.sh
echo "Hello, world!"
chmod +x hello.sh
./hello.sh
# Hello, world!
bash hello.sh       # also works without chmod
bash --version      # check which version you have

#!/usr/bin/env bash busca Bash en el PATH, lo que es más portable que fijar /bin/bash. Si un script usa solo funciones de POSIX, puede usar #!/bin/sh, pero entonces no se permite la sintaxis exclusiva de Bash, como [[ ]] y los arreglos.

Variables, comillas y expansión

Asigna con name=value y lee con $name o ${name}. Toda variable es una cadena; Bash solo hace aritmética entera dentro de $(( )). Usa comillas dobles alrededor de las expansiones para que los espacios y los caracteres especiales se conserven.

name="Ada"                 # no spaces around =
count=3
echo "Hello, $name"        # Hello, Ada
echo "Next: $((count + 1))"  # Next: 4  (integer arithmetic)
today=$(date +%Y-%m-%d)    # command substitution
echo "Today is $today"
file="my report.txt"
echo "${file%.txt}.pdf"    # my report.pdf  (strip suffix)
echo "${#name}"            # 3  (string length)
echo "${missing:-default}" # default  (fallback value)
SintaxisSignificado
"$var"valor de var, conservado como una sola palabra
'text'texto literal, sin ninguna expansión
$(command)salida de un comando
$((a + b))aritmética de enteros
$1, $2, $@, $#argumentos del script o de la función, todos los argumentos, número de argumentos
$?código de salida del último comando (0 significa éxito)

Condiciones y case

if ejecuta un comando y comprueba su código de salida. El comando de prueba [[ ]] compara cadenas y comprueba archivos; (( )) compara números. case compara un valor con patrones glob.

#!/usr/bin/env bash
file="${1:-notes.txt}"

if [[ -f "$file" ]]; then
  echo "$file exists"
elif [[ -d "$file" ]]; then
  echo "$file is a directory"
else
  echo "$file not found"
fi

n=7
if (( n % 2 == 1 )); then echo "$n is odd"; fi

case "$file" in
  *.txt) echo "text file" ;;
  *.sh)  echo "shell script" ;;
  *)     echo "something else" ;;
esac
PruebaEs verdadera cuando
[[ -f path ]]path es un archivo normal
[[ -d path ]]path es un directorio
[[ -z "$s" ]]la cadena está vacía
[[ "$a" == "$b" ]]las cadenas son iguales
(( a > b ))el número a es mayor que b
[[ $a -lt $b ]]el número a es menor que b (estilo antiguo)

Bucles

Bash tiene for sobre una lista de palabras, un for (( )) al estilo de C, while y until. Para leer un archivo línea por línea se usa while read.

for fruit in apple banana cherry; do
  echo "fruit: $fruit"
done

for (( i = 1; i <= 3; i++ )); do
  echo "i = $i"
done

for f in *.log; do
  [[ -e "$f" ]] || continue   # skip if no .log files match
  echo "log file: $f"
done

while IFS= read -r line; do
  echo "line: $line"
done < input.txt

IFS= read -r lee cada línea exactamente como está escrita: IFS= conserva los espacios iniciales y -r conserva las barras invertidas.

Funciones

Define una función con name() { ... } y llámala como un comando. Los argumentos llegan como $1, $2, etc. Una función devuelve un código de salida (de 0 a 255) con return; para devolver datos, imprímelos y captura la salida con $( ).

greet() {
  local name="${1:-world}"   # local variable, default value
  echo "Hello, $name!"
}

is_even() {
  (( $1 % 2 == 0 ))          # exit status 0 means true
}

greet                         # Hello, world!
greet "Ann"                   # Hello, Ann!
msg=$(greet "Bo")             # capture output
echo "Got: $msg"              # Got: Hello, Bo!

if is_even 4; then echo "4 is even"; fi

Arreglos y arreglos asociativos

Los arreglos indexados contienen una lista de cadenas. Los arreglos asociativos asignan valores a claves de tipo cadena y requieren Bash 4 o posterior, por lo que no están disponibles en el antiguo Bash 3.2 que incluye macOS.

langs=("bash" "python" "go")
langs+=("rust")
echo "${langs[0]}"           # bash
echo "${#langs[@]}"          # 4
for l in "${langs[@]}"; do echo "- $l"; done

# associative array (Bash 4 or later)
declare -A port
port[http]=80
port[https]=443
echo "${port[https]}"        # 443
for key in "${!port[@]}"; do
  echo "$key -> ${port[$key]}"
done

Escribe siempre "${array[@]}" entre comillas para recorrer los elementos; así cada elemento se mantiene como una sola palabra aunque contenga espacios.

Tuberías y redirección

La característica distintiva de Bash es componer programas pequeños. Una tubería | envía la salida de un comando a la entrada del siguiente. Las redirecciones envían la salida a archivos o leen la entrada de ellos. El descriptor de archivo 1 es la salida estándar y el 2 es el error estándar.

ls -l > files.txt            # write stdout to a file (overwrite)
echo "more" >> files.txt     # append
grep error app.log 2> /dev/null   # discard error messages
make > build.log 2>&1        # stdout and stderr to the same file

# count the 3 most common words in a file
tr -cs '[:alpha:]' '\n' < book.txt | tr '[:upper:]' '[:lower:]' |
  sort | uniq -c | sort -rn | head -n 3

Manejo de errores y scripts seguros

De forma predeterminada, Bash sigue adelante después de que un comando falla. Muchos scripts empiezan con set -euo pipefail: -e termina ante un error no tratado, -u considera errores las variables no definidas y -o pipefail hace que una tubería falle si falla cualquier comando de ella. trap ejecuta código de limpieza cuando el script termina.

#!/usr/bin/env bash
set -euo pipefail            # stop on errors, unset vars, pipe failures

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT     # clean up when the script exits

if ! grep -q "ready" status.txt; then
  echo "status is not ready" >&2   # errors go to stderr
  exit 1
fi

cp config.txt "$tmp" || { echo "copy failed" >&2; exit 2; }
echo "ok"

Usa || para tratar un fallo en una sola línea y && para ejecutar un comando solo si el anterior tuvo éxito. set -e tiene excepciones (por ejemplo, los comandos dentro de condiciones if no lo activan), así que comprueba de forma explícita los comandos importantes.

Un script pequeño y completo: copias de seguridad con fecha

Este script archiva un directorio en un archivo .tar.gz con marca de tiempo y conserva solo los cinco archivos más recientes. Usa argumentos, valores predeterminados, pruebas, sustitución de comandos y tuberías.

#!/usr/bin/env bash
# backup.sh: copy a directory into a dated .tar.gz archive
set -euo pipefail

usage() { echo "usage: $0 SOURCE_DIR [DEST_DIR]" >&2; exit 1; }

[[ $# -ge 1 ]] || usage
src="$1"
dest="${2:-$HOME/backups}"
[[ -d "$src" ]] || { echo "not a directory: $src" >&2; exit 1; }

mkdir -p "$dest"
stamp=$(date +%Y%m%d-%H%M%S)
archive="$dest/$(basename "$src")-$stamp.tar.gz"

tar -czf "$archive" -C "$(dirname "$src")" "$(basename "$src")"
size=$(du -h "$archive" | cut -f1)
echo "Created $archive ($size)"

# keep only the 5 newest archives for this source
ls -1t "$dest"/"$(basename "$src")"-*.tar.gz | tail -n +6 | while read -r old; do
  rm -f -- "$old"
  echo "Removed $old"
done
./backup.sh ~/projects/site
# Created /home/ann/backups/site-20261031-093000.tar.gz (2.1M)

Siguientes pasos

  • El GNU Bash Reference Manual, la documentación oficial.
  • man bash y help (por ejemplo help read) en tu terminal.
  • ShellCheck, una herramienta de análisis estático que explica los errores comunes en los scripts.
  • La especificación de la shell POSIX si necesitas scripts que también funcionen en sh, dash o BusyBox.

Preguntas frecuentes

¿Cuál es la diferencia entre Bash y sh?

sh es el estándar de la shell POSIX, implementado por programas como dash. Bash es compatible con POSIX sh, pero añade características como [[ ]], arreglos, (( )) y la expansión de llaves.

¿Cuál es la diferencia entre Bash y zsh?

Ambos son compatibles para la mayoría de los comandos cotidianos. zsh añade funciones interactivas y es la shell predeterminada en macOS; los scripts escritos con un shebang de bash siguen ejecutándose con Bash.

¿Por qué mi script dice permission denied?

El archivo no es ejecutable. Ejecuta chmod +x script.sh o inícialo con bash script.sh.

¿Cómo paso argumentos a un script de Bash?

Escríbelos después del nombre del script, como en ./script.sh one two. Dentro del script son $1 y $2, $@ los contiene todos y $# es su número.

¿Debo escribir programas grandes en Bash?

Bash es ideal para unir comandos. Cuando un script se vuelve complejo, necesita estructuras de datos o un manejo de errores cuidadoso, un lenguaje como Python suele ser más fácil de mantener.

Todos los tutoriales de programación