Skip to content

Bash Hoja de referencia

Shell de Unix y lenguaje de comandos para scripting y automatización.

01

Variables y Cadenas

Variables y Asignación

Las variables de Bash son cadenas sin tipo por defecto; la aritmética funciona si el valor es numérico. NUNCA ponga espacios alrededor de = en las asignaciones — 'name = Alice' intenta ejecutar un comando llamado name. Use $(...) para sustitución de comandos (preferido sobre los backticks). export hace una variable disponible para procesos hijos; readonly la hace inmutable. Las variables de entorno (exportadas) son heredadas por scripts y programas que lance.

bash
# no spaces around = in assignments
name="Alice"
age=30
PI=3.14
active=true

# use variables with $ prefix
echo "Hello, $name!"
echo "Age: $age"

# command substitution: capture command output
today=$(date +%Y-%m-%d)
files=$(ls | wc -l)
echo "Today is $today, $files files"

# readonly and environment variables
readonly MAX=100
export PATH="$PATH:/opt/bin"
env | grep PATH

Expansión de Parámetros de Cadena

La expansión de parámetros es una de las características más potentes de Bash, encerrada en ${...}. ${#var} da la longitud; ${var:offset:length} extrae subcadenas (offset negativo cuenta desde el final — note el espacio antes de -). ,, y ^^ convierten mayúsculas/minúsculas (Bash 4+). :- proporciona valores predeterminados sin establecer la variable; := la establece como efecto secundario; :? aborta con un error — excelente para verificaciones de argumentos requeridos en scripts.

bash
s="Hello World"
echo ${#s}                # length: 11
echo ${s:0:5}             # substring: Hello
echo ${s:6}               # from index 6: World
echo ${s: -5}             # last 5 chars: World

# case conversion (Bash 4+)
echo ${s,,}               # lowercase: hello world
echo ${s^^}               # uppercase: HELLO WORLD
echo ${s^l}               # capitalize first if 'l': Hello World

# default values
echo ${name:-Guest}       # use Guest if name unset
echo ${name:=Guest}       # assign Guest if unset, then use
echo ${name:?required}    # error and exit if unset

Búsqueda y Reemplazo de Cadenas

El patrón / sustituye; // sustituye todos; # coincide al inicio; % coincide al final. ## y %% son voraces (coincidencia más larga) mientras que # y % no son voraces. Estos son esenciales para manipulación de rutas: ${path##*/} extrae el basename (como el comando basename), y ${path%/*} extrae el directorio (como dirname). Dominar esto evita muchas llamadas a comandos externos como sed y cut.

bash
s="Hello World Hello"

# replace first occurrence
echo ${s/Hello/Hi}        # Hi World Hello

# replace all occurrences
echo ${s//Hello/Hi}       # Hi World Hi

# replace at beginning / end
echo ${s/#Hello/Hi}       # Hi World Hello (prefix)
echo ${s/%Hello/Hi}       # Hello World Hello (no suffix match)

# delete patterns (replace with empty)
echo ${s//Hello/}         #  World 
path="/usr/local/bin/app"
echo ${path##*/}          # app (longest prefix)
echo ${path%/*}           # /usr/local/bin (shortest suffix)

# extract filename and extension
file="report.tar.gz"
echo ${file%.*}           # report.tar (remove ext)
echo ${file##*.}          # gz (keep ext)

Comillado y Caracteres Especiales

Las comillas simples preservan todo literalmente — sin expansión de variables, sin procesamiento de escapes (no puede ni siquiera incluir una comilla simple dentro de comillas simples). Las comillas dobles permiten la expansión de $, backtick y \ mientras preservan espacios y caracteres especiales — casi siempre prefiera comillas dobles alrededor de variables para prevenir bugs de word-splitting y globbing. Use $'...' para escapes ANSI-C como \n y \t.

bash
# single quotes: literal, no expansion
echo 'Hello $USER'         # Hello $USER

# double quotes: expansion happens
echo "Hello $USER"         # Hello alice

# escape special chars with backslash
echo "Price: \\$5.00"      # Price: $5.00
echo 'It\'s a test'       # It's a test (escape inside single)

# concatenate strings
a="Hello"
b="World"
c="$a, $b!"
echo $c                    # Hello, World!

# multi-line string
msg="Line 1
Line 2
Line 3"
echo "$msg"

Read y Entrada de Usuario

read captura la entrada del usuario. -p muestra un prompt, -t establece un timeout (devuelve no-cero), -a lee en un array. Al leer archivos línea por línea, siempre use 'IFS= read -r' — IFS= preserva los espacios en blanco iniciales/finales, y -r desactiva el escape de backslash para que los backslashes se preserven literalmente. Este es el patrón seguro canónico para procesar archivos línea por línea.

bash
# read a single value
echo -n "Enter your name: "
read name
echo "Hi, $name!"

# read multiple values
read -p "First and last: " first last

# read with timeout (seconds)
read -t 5 -p "Quick! " answer || echo "too slow"

# read into an array
echo "Enter colors:"
read -a colors
echo "First: ${colors[0]}"

# read line by line from a file
while IFS= read -r line; do
    echo ">>> $line"
done < file.txt
02

Arrays

Arrays Indexados

Los arrays de Bash son de índice cero. @ y * expanden a todos los elementos — siempre entrecomíllelos ("${arr[@]}") para manejar correctamente elementos con espacios. ${#arr[@]} da el conteo. += añade. unset deja un hueco en los índices; para reindexar, use arr=("${arr[@]}"). El slicing con ${arr[@]:start:count} devuelve un subconjunto.

bash
# create and populate
fruits=("apple" "banana" "cherry")
fruits[3]="date"

# access elements
echo ${fruits[0]}         # apple
echo ${fruits[@]}         # all: apple banana cherry date
echo ${fruits[*]}         # same as @ in most contexts

# count and slice
echo ${#fruits[@]}        # count: 4
echo ${fruits[@]:1:2}     # elements 1-2: banana cherry

# append
fruits+=("elderberry")

# iterate
for f in "${fruits[@]}"; do
    echo "- $f"
done

# delete an element
unset fruits[1]            # removes "banana" (gap remains)

Arrays Asociativos (Mapas)

Los arrays asociativos (declare -A) mapean claves de cadena a valores, como diccionarios en otros lenguajes — disponibles en Bash 4+. Use ${!arr[@]} para obtener claves y ${arr[@]} para valores. El test -v verifica si existe una clave. A diferencia de los arrays indexados, las claves son cadenas arbitrarias, así que entrecomillar claves con espacios es esencial. Estos requieren Bash 4+ (macOS incluye Bash 3 por defecto).

bash
# must declare before use (Bash 4+)
declare -A ages

# key-value assignment
ages[alice]=30
ages[bob]=25
ages["carol smith"]=28

# access by key
echo ${ages[alice]}       # 30
echo ${ages[bob]}         # 25

# all keys and values
echo ${!ages[@]}          # keys: alice bob carol smith
echo ${ages[@]}           # values: 30 25 28
echo ${#ages[@]}          # count: 3

# iterate keys
for name in "${!ages[@]}"; do
    echo "$name is ${ages[$name]}"
done

# check if key exists
if [[ -v ages[alice] ]]; then
    echo "alice exists"
fi

Patrones de Iteración de Arrays

Hay tres estilos principales de iteración: por valor (for x in "${arr[@]}"), por índice (for i in "${!arr[@]}") y estilo C. La forma por índice es útil cuando necesita la posición. Siempre entrecomille "${arr[@]}" para que los elementos que contienen espacios se preserven como elementos únicos. Construir arrays con += en un bucle es la forma idiomática de acumular resultados.

bash
arr=(one two three four five)

# iterate by value
for item in "${arr[@]}"; do
    echo "$item"
done

# iterate by index
for i in "${!arr[@]}"; do
    echo "[$i] = ${arr[$i]}"
done

# C-style indexed loop
for ((i=0; i<${#arr[@]}; i++)); do
    echo "${arr[$i]}"
done

# map/transform: build a new array
upper=()
for w in "${arr[@]}"; do
    upper+=("$(echo $w | tr a-z A-Z)")
done
echo "${upper[@]}"

Array desde Salida de Comando

mapfile/readarray (Bash 4+) lee líneas en un array de una sola vez — mucho más rápido que un bucle while para archivos grandes. Para dividir una cadena delimitada, establezca IFS y use read -ra. Para nombres de archivo (que pueden contener espacios o nuevas líneas), siempre use find -print0 con read -d '' para dividir en bytes nulos — el único delimitador seguro para nombres de archivo. La sustitución de proceso < <(...) alimenta el bucle sin un subshell.

bash
# split a string into an array
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo ${parts[2]}          # c

# read lines of a file into an array
mapfile -t lines < file.txt
echo "${lines[0]}"        # first line
echo "${#lines[@]}"       # line count

# alternative: readarray (same as mapfile)
readarray -t lines2 < file.txt

# from command output (words split on IFS)
files=($(ls *.txt))
echo "${files[@]}"

# safer: read null-delimited output
while IFS= read -r -d '' f; do
    files+=("$f")
done < <(find . -name "*.txt" -print0)

Variables Especiales

Estas variables especiales son esenciales para scripting. $0 es la ruta del script; $1-$9 son argumentos posicionales (use ${10} para dos dígitos). Siempre entrecomille "$@" para preservar argumentos con espacios — "$*" los une en una cadena. $? da el estado de salida (0 = éxito) y es la base de toda verificación de errores. $$ y $! son útiles para archivos PID y gestión de procesos. shift descarta $1 y desplaza el resto hacia abajo.

bash
# positional parameters
echo $0       # script name
echo $1       # first argument
echo $#       # argument count
echo $@       # all arguments (separate words)
echo "$@"     # all arguments (preserves quoting)
echo $*       # all arguments (single string)

# process and shell info
echo $$       # current shell PID
echo $!       # PID of last background command
echo $?       # exit status of last command (0 = success)
echo $-       # current shell option flags
echo $_       # last argument of previous command

# shift arguments
echo "$1 $2"  # a b
shift
echo "$1 $2"  # b c

# iterate all args safely
for arg in "$@"; do
    echo "arg: $arg"
done
03

Flujo de Control y Tests

If / Elif / Else

[ ] es el comando de test POSIX (portable pero limitado); [[ ]] es la versión mejorada de Bash que soporta pattern matching (== con wildcards), regex (=~), operadores && / || y no necesita entrecomillar variables. Prefiera [[ ]] en scripts de Bash. Siempre entrecomille variables en [ ] para evitar errores de sintaxis con valores vacíos o que contienen espacios. Los tests -f, -r, -d, -e verifican existencia y atributos de archivo.

bash
# basic if
if [ "$age" -ge 18 ]; then
    echo "adult"
elif [ "$age" -ge 13 ]; then
    echo "teen"
else
    echo "child"
fi

# modern [[ ]] test (Bash-specific, safer)
if [[ $name == "Alice" ]]; then
    echo "hi Alice"
fi

# pattern matching in [[ ]]
if [[ $file == *.txt ]]; then
    echo "text file"
fi

# regex matching
if [[ $email =~ ^[a-z]+@[a-z]+.[a-z]+$ ]]; then
    echo "valid email"
fi

# multiple conditions
if [[ -f file.txt && -r file.txt ]]; then
    echo "readable file"
fi

Operadores de Test

Los tests de archivo (-f, -d, -r, etc.) verifican atributos del sistema de archivos. Los tests de cadena usan = y != (o == en [[ ]]); -z verifica vacío, -n no vacío. Las comparaciones de enteros usan -eq, -ne, -lt, -gt, -le, -ge — NO use < > para enteros en [ ] (¡redirigen!). En [[ ]] y (( )) puede usar los familiares operadores < > <= >=. Memorizar estos operadores es esencial para escribir condicionales correctos.

bash
# file tests
[ -f file ]       # exists and is regular file
[ -d dir ]        # exists and is directory
[ -e path ]       # exists (any type)
[ -r file ]       # readable
[ -w file ]       # writable
[ -x file ]       # executable
[ -s file ]       # exists and is non-empty
[ file1 -nt file2 ]  # newer than
[ file1 -ot file2 ]  # older than

# string tests
[ -z "$s" ]       # empty string
[ -n "$s" ]       # non-empty string
[ "$a" = "$b" ]   # equal (use == in [[ ]])
[ "$a" != "$b" ]  # not equal

# integer tests
[ $n -eq 5 ]      # equal
[ $n -ne 5 ]      # not equal
[ $n -lt 5 ]      # less than
[ $n -gt 5 ]      # greater than
[ $n -le 5 ]      # less or equal
[ $n -ge 5 ]      # greater or equal

Sentencia Case

case es el equivalente en Bash de switch — compara un valor contra patrones glob. | separa alternativas. ;; termina una rama (como break). El patrón *) es el predeterminado. Los patrones soportan wildcards (*, ?, [abc]) pero no regex completo. case es más limpio que largas cadenas if-elif para despachar sobre un único valor, y es la forma estándar de construir subcomandos de línea de comandos (start/stop/restart).

bash
case $1 in
    start)
        echo "Starting service..."
        systemctl start app
        ;;
    stop)
        echo "Stopping service..."
        systemctl stop app
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    status)
        systemctl status app
        ;;
    --help|-h)
        echo "Usage: $0 {start|stop|restart|status}"
        ;;
    *)
        echo "Unknown command: $1" >&2
        exit 1
        ;;
esac

# pattern matching in case
case $file in
    *.jpg|*.png)    echo "image" ;;
    *.mp3|*.wav)    echo "audio" ;;
    *.txt)          echo "text" ;;
    *)              echo "other" ;;
esac

Aritmética y (( ))

$(( )) evalúa expresiones aritméticas y devuelve el resultado. Dentro de (( )), los nombres de variables no necesitan $ — solo use el nombre. (( )) como comando devuelve estado de salida 0 (verdadero) si el resultado es no-cero, lo que lo hace ideal para condiciones numéricas. Bash solo hace aritmética de enteros; para coma flotante use bc o awk. Operadores: + - * / % ** y operadores de comparación y bitwise estilo C todos funcionan.

bash
# arithmetic expansion
x=5
y=$((x + 3))
echo $y              # 8
echo $((x * 2))      # 10
echo $((2 ** 10))    # 1024 (exponent)
echo $((17 % 5))     # 2 (modulo)
echo $((17 / 5))     # 3 (integer division)

# (( )) condition: no $ needed, returns exit status
x=10
if (( x > 5 && x < 20 )); then
    echo "in range"
fi

# increment / decrement
((x++))             # post-increment
((x--))             # post-decrement
((++x))             # pre-increment

# bitwise operators
echo $((5 & 3))     # 1 (AND)
echo $((5 | 2))     # 7 (OR)
echo $((5 << 1))    # 10 (left shift)

Cortocircuito y Ternario

&& y || son operadores de cortocircuito que también sirven como flujo de control conciso: 'cmd1 && cmd2' ejecuta cmd2 solo si cmd1 tiene éxito; 'cmd1 || cmd2' ejecuta cmd2 solo si cmd1 falla. Esto reemplaza un if-else simple en una línea. El patrón 'A && B || C' imita un ternario pero es sutilmente buggy si B puede fallar — para código robusto, use una sentencia if real. Los dos puntos (:) es un comando no-op útil para ramas vacías.

bash
# && and || as short-circuit control flow
[[ -f file.txt ]] && echo "exists"
[[ -f file.txt ]] || echo "missing"

# combined: do this OR fail
mkdir -p build && cd build && make || exit 1

# ternary-like (Bash doesn't have a real ternary)
result=$([[ $x -gt 0 ]] && echo "positive" || echo "non-positive")

# default value via &&
echo ${name:-"anonymous"}

# run command, fallback on failure
ping -c1 host && echo "up" || echo "down"

# null command as no-op
if condition; then
    :    # do nothing
fi
04

Bucles e Iteración

Bucles For

El bucle for itera sobre una lista de palabras. La expansión de llaves {1..5} genera secuencias (con paso opcional {start..end..step}). El for estilo C ((init; cond; update)) es mejor para contadores numéricos. Al iterar archivos con *.txt, los globs sin entrecomillar se expanden de forma segura, pero si ningún archivo coincide obtiene el literal '*.txt' — habilite shopt -s nullglob para obtener una lista vacía en su lugar.

bash
# iterate a list
for fruit in apple banana cherry; do
    echo "$fruit"
done

# iterate an array
colors=("red" "green" "blue")
for c in "${colors[@]}"; do
    echo "Color: $c"
done

# range with brace expansion
for i in {1..5}; do
    echo "Number: $i"
done

# range with step
for i in {0..20..2}; do echo $i; done    # 0 2 4 ... 20

# C-style for loop
for ((i=0; i<5; i++)); do
    echo "Iteration $i"
done

# iterate command output
for file in *.txt; do
    echo "Processing $file"
done

Bucles While y Until

while se ejecuta mientras la condición tenga éxito (exit 0); until se ejecuta hasta que tenga éxito — son opuestos. El patrón 'while read' es la forma canónica de procesar archivos línea por línea; siempre use 'IFS= read -r' por seguridad. Note que pipear a un bucle while lo ejecuta en un subshell, por lo que los cambios de variables no persisten — use sustitución de proceso '< <(cmd)' cuando necesite mantener variables.

bash
# while: runs while condition is true
count=0
while [ $count -lt 5 ]; do
    echo "Count: $count"
    ((count++))
done

# read lines from a file
while IFS= read -r line; do
    echo "Line: $line"
done < input.txt

# infinite loop with break
while true; do
    read -p "Quit? (y/n) " ans
    [[ $ans == "y" ]] && break
done

# until: runs until condition is true (opposite of while)
n=0
until [ $n -ge 3 ]; do
    echo "n=$n"
    ((n++))
done

# while reading from a pipeline
seq 1 5 | while read n; do
    echo "got $n"
done

Break, Continue y Select

break sale del bucle (break N sale de N bucles anidados); continue salta a la siguiente iteración. select crea un menú numerado interactivo desde una lista — itera hasta que se rompe. Estas sentencias de control funcionan en bucles for, while y until. La forma break N es esencial para escapar de bucles profundamente anidados pero debería usarse con moderación ya que puede hacer el código más difícil de seguir.

bash
# break exits the loop
for i in {1..10}; do
    [[ $i -eq 5 ]] && break
    echo $i       # prints 1 2 3 4
done

# continue skips to next iteration
for i in {1..6}; do
    (( i % 2 == 0 )) && continue
    echo $i       # prints 1 3 5 (odd only)
done

# break out of nested loops
for i in 1 2 3; do
    for j in a b c; do
        [[ $j == "b" ]] && break 2   # break both loops
        echo "$i$j"
    done
done

# select menu
select opt in "Start" "Stop" "Quit"; do
    case $opt in
        Start) echo "starting" ;;
        Stop)  echo "stopping" ;;
        Quit)  break ;;
        *)     echo "invalid" ;;
    esac
done

Iterar Archivos de Forma Segura

Parsear la salida de ls es un bug clásico — los nombres de archivo con espacios, nuevas líneas o caracteres especiales lo rompen. Use globs de shell (*.txt) directamente en bucles for, y entrecomille la variable. Para búsquedas recursivas o complejas, use find -print0 pipeado a 'read -d ''' — los bytes nulos son el único delimitador seguro para nombres de archivo. Habilite nullglob para que los globs sin coincidencia produzcan una lista vacía en lugar del patrón literal.

bash
# BAD: breaks on filenames with spaces
for f in $(ls *.txt); do
    cat "$f"
done

# GOOD: glob expands safely
for f in *.txt; do
    [[ -f "$f" ]] || continue
    cat "$f"
done

# BEST: handle spaces, newlines, and no matches
shopt -s nullglob dotglob
for f in *.txt; do
    echo "Processing: $f"
done

# find with -print0 and while read -d ''
while IFS= read -r -d '' f; do
    echo "Found: $f"
done < <(find . -type f -name "*.txt" -print0)

# process files modified today
for f in $(find . -mtime -1 -print); do
    echo "Recent: $f"
done

Rangos y Generación de Secuencias

La expansión de llaves {a..b} genera secuencias en tiempo de parseo — rápida e integrada. Soporta letras, números, relleno con ceros y pasos ({start..end..step}). seq es un comando externo con más opciones de formato (-f para estilo printf). La expansión de llaves también crea múltiples argumentos: echo file{1..3}.{txt,log} genera 6 nombres de archivo. Prefiera la expansión de llaves para rangos simples; use seq cuando necesite formato personalizado.

bash
# brace expansion (not a loop, but generates lists)
echo {1..10}                 # 1 2 3 4 5 6 7 8 9 10
echo {a..e}                  # a b c d e
echo {01..10}                # zero-padded: 01 02 ... 10
echo file{1..3}.txt          # file1.txt file2.txt file3.txt

# seq command
seq 1 5                      # 1 2 3 4 5
seq 1 2 10                   # 1 3 5 7 9 (step 2)
seq -f "%03d" 1 5            # 001 002 003 004 005

# use seq in a for loop
for i in $(seq 1 5); do
    echo $i
done

# generate a list of dates
for d in $(seq -f "2024-01-%02g" 1 31); do
    echo "$d"
done
05

Funciones

Definir y Llamar Funciones

Las funciones en Bash se definen con name() o function name. Llámelas por nombre con argumentos separados por espacios (sin paréntesis). SIEMPRE use 'local' para variables dentro de funciones para evitar contaminar el scope global — sin ello, las asignaciones se filtran. Las funciones de Bash no pueden devolver valores directamente (return es un estado de salida 0-255); para devolver una cadena, haga echo y capture con $().

bash
# two equivalent syntaxes
greet() {
    echo "Hello, $1!"
}
function greet2() {
    echo "Hi, $1!"
}

# call with arguments (no parens)
greet "Alice"               # Hello, Alice!
greet2 "Bob"                # Hi, Bob!

# local variables (function-scoped)
counter() {
    local count=0
    ((count++))
    echo $count
}

# return a value via echo + command substitution
add() {
    echo $(($1 + $2))
}
result=$(add 3 4)           # 7

Argumentos y Parámetros

Dentro de una función, $1, $@, $# se refieren a los argumentos de la función, no a los del script — hacen shadow de los parámetros a nivel de script. $FUNCNAME contiene el nombre de la función (útil para depuración). shift dentro de una función solo afecta a los parámetros de la función. Para pasar los argumentos del script a una función, use func "$@". Este shadowing es por qué las funciones son bloques de construcción reutilizables.

bash
# all positional params work inside functions
show_args() {
    echo "Function name: $FUNCNAME"
    echo "First arg:  $1"
    echo "Second arg: $2"
    echo "All args:   $@"
    echo "Arg count:  $#"
}

show_args a b c
# Function name: show_args
# First arg:  a
# Second arg: b
# All args:   a b c
# Arg count:  3

# pass all script args to a function
process "$@"

# shift within a function (local to it)
parse() {
    while (($#)); do
        echo "arg: $1"
        shift
    done
}

Valores de Retorno y Estado de Salida

El 'return' de Bash solo establece un estado de salida (0-255), así que las funciones sirven también como tests booleanos. Para devolver una cadena, haga echo y capture con $(). Para múltiples valores, use un nameref (local -n) — Bash 4.3+ — que permite a la función asignar a una variable del llamador por nombre. Esta es la forma más limpia de devolver datos complejos. Evite usar variables globales para valores de retorno ya que hace las funciones no reentrantes.

bash
# return sets exit status (0-255), not a value
is_even() {
    if (($1 % 2 == 0)); then
        return 0    # true/success
    else
        return 1    # false/failure
    fi
}

# use as a condition
if is_even 4; then
    echo "4 is even"
fi

# chain with && and ||
is_even 4 && echo "yes" || echo "no"

# return a string via echo (capture with $())
to_upper() {
    echo "${1^^}"
}
UP=$(to_upper hello)        # HELLO

# return multiple values via global or nameref
set_pair() {
    local -n _r=$1
    _r=(10 20)
}
set_pair result
echo "${result[@]}"       # 10 20

Ámbito de Variables

Las variables en Bash son globales por defecto — ¡incluso cuando se asignan dentro de una función! Esta es una fuente común de bugs. Siempre use 'local' para variables internas de función. 'declare -g' crea explícitamente una global desde dentro de una función. La recursión funciona pero es lenta en Bash (cada llamada hace fork de un subshell para $()); úsela con moderación. El ejemplo factorial muestra el patrón: variables locales + llamada recursiva + aritmética.

bash
# global by default
x=1
modify() {
    x=2          # changes the GLOBAL x!
    local y=3    # local to function
    echo "inside: x=$x y=$y"
}

modify
echo "outside: x=$x"       # x=2 (modified!)
echo "outside: y=$y"       # empty (y is local)

# declare -g for global inside a function
set_global() {
    declare -g NEW_VAR=42
}
set_global
echo $NEW_VAR              # 42

# recursion with local
factorial() {
    local n=$1
    if (( n <= 1 )); then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}
echo $(factorial 5)        # 120

Bibliotecas y Sourcing

source (o .) ejecuta un archivo en el shell actual, por lo que sus funciones y variables se vuelven disponibles — así es como construye bibliotecas reutilizables. Un patrón común es un utils.sh con funciones helper que los scripts hacen source. A diferencia de ejecutar un script (que corre en un subshell), el código fuente puede modificar el entorno del llamador. Así es también como funcionan .bashrc y .bash_profile — se les hace source al inicio del shell.

bash
# utils.sh — a reusable library
#!/bin/bash
log() {
    echo "[$(date +%H:%M:%S)] $*" >&2
}

die() {
    log "ERROR: $*"
    exit 1
}

confirm() {
    read -p "$1 [y/N] " ans
    [[ $ans =~ ^[Yy]$ ]]
}

# main.sh — use the library
source ./utils.sh        # or . ./utils.sh

log "Starting script"
confirm "Continue?" || die "aborted"
log "Done"

# source vs execute
# source: runs in current shell (shares variables)
# ./script.sh: runs in a subshell (isolated)
06

Procesamiento de Texto

grep — Búsqueda de Patrones

grep encuentra líneas que coinciden con un patrón. -i ignora mayúsculas/minúsculas, -v invierte, -n muestra números de línea, -r recursa, -E usa regex extendida (como +, |, {}). -A/-B/-C muestran líneas de contexto alrededor de las coincidencias — invaluable para entender errores. Use --include para filtrar tipos de archivo durante la búsqueda recursiva. grep devuelve estado de salida 0 si se encuentra una coincidencia, lo que lo hace útil en condiciones: if grep -q pattern file; then...

bash
# basic search
grep "error" app.log
grep -i "error" app.log          # case-insensitive
grep -v "debug" app.log          # invert (non-matching lines)
grep -c "error" app.log          # count matches
grep -n "error" app.log          # line numbers
grep -w "error" app.log          # whole word only

# recursive search
grep -rn "TODO" src/             # recursive + line numbers
grep -rl "TODO" src/             # files with matches only

# extended regex (or use egrep)
grep -E "error|warn|fatal" log
grep -E "^[0-9]{4}-" log         # lines starting with date

# context lines
grep -A 2 "error" log            # 2 lines after
grep -B 2 "error" log            # 2 lines before
grep -C 2 "error" log            # 2 lines before and after

# search multiple files
grep "function" *.js --include="*.js" -r .

sed — Editor de Flujo

sed edita flujos de texto de forma no interactiva. El comando s sustituye; g lo hace global. -i edita in situ (¡siempre pruebe sin -i primero!). El delimitador puede ser cualquier carácter — use | o # para rutas y evitar escapar barras. sed procesa línea por línea, por lo que operaciones multilínea necesitan el comando N o el hold space. Para ediciones complejas, awk o perl pueden ser más claros. Siempre entrecomille scripts sed para prevenir la expansión del shell.

bash
# substitute (replace)
sed 's/old/new/' file.txt           # first occurrence per line
sed 's/old/new/g' file.txt          # all occurrences (global)
sed 's/old/new/3' file.txt          # 3rd occurrence only
sed 's/old/new/gi' file.txt         # global + case-insensitive

# in-place editing
sed -i 's/foo/bar/g' file.txt       # edit file in place
sed -i.bak 's/foo/bar/g' file.txt   # keep a backup

# delete lines
sed '/^#/d' file.txt                # delete comment lines
sed '/^$/d' file.txt                # delete blank lines
sed '5d' file.txt                   # delete line 5
sed '5,10d' file.txt                # delete lines 5-10

# print specific lines
sed -n '10,20p' file.txt            # print lines 10-20
sed -n '/pattern/p' file.txt        # print matching lines

# use a different delimiter for paths
sed 's|/usr/local|/opt|g' paths.txt

# multiple commands
sed -e 's/a/A/g' -e 's/b/B/g' file.txt

awk — Procesamiento de Columnas

awk es un mini lenguaje de programación para datos columnares. $1, $2... son campos; $0 es la línea completa; $NF es el último campo. -F establece el separador de entrada; OFS el de salida. BEGIN se ejecuta antes del procesamiento, END después. NR es el número de registro (línea); NF es el conteo de campos. awk es ideal para procesamiento de CSV/TSV, análisis de logs y generación de informes — mucho más potente que cut para cualquier cosa más allá de la extracción simple.

bash
# print columns (default separator: whitespace)
awk '{print $1}' file.txt           # first column
awk '{print $1, $3}' file.txt       # columns 1 and 3
awk '{print $NF}' file.txt          # last column
awk -F: '{print $1}' /etc/passwd    # split on :

# filter and print
awk '$3 > 100' file.txt             # lines where col 3 > 100
awk '$1 == "ERROR" {print $2}' log  # col 2 of ERROR lines
awk 'NR == 5' file.txt              # 5th line only
awk 'NR >= 10 && NR <= 20' file.txt # lines 10-20

# BEGIN and END blocks
awk 'BEGIN {print "Start"} {sum += $1} END {print sum}' nums.txt

# field separator and output
awk -F, 'BEGIN {OFS="|"} {print $1, $2}' csv.txt

# count lines matching a pattern
awk '/error/' file.txt              # like grep
awk '/error/ {count++} END {print count}' log

# built-in variables: NR (row), NF (fields), FS, OFS
awk '{print NR, NF, $0}' file.txt

cut, tr, sort y uniq

cut es extracción simple de campos/caracteres — rápido pero limitado (sin soporte de comillado). tr traduce caracteres (genial para conversión de mayúsculas/minúsculas o intercambio de delimitadores) y -d elimina. sort ordena líneas (-n numérico, -r inverso, -k campo). uniq solo elimina duplicados ADYACENTES, así que siempre pipee a través de sort primero. uniq -c con sort -rn es el patrón clásico para análisis de frecuencia: 'sort | uniq -c | sort -rn' muestra las líneas más comunes.

bash
# cut: extract fields or characters
cut -d: -f1 /etc/passwd            # field 1, delimiter :
cut -c1-5 file.txt                 # characters 1-5
cut -d, -f2,3 data.csv             # fields 2 and 3

# tr: translate or delete characters
echo "Hello" | tr 'a-z' 'A-Z'      # HELLO
echo "a,b,c" | tr ',' ' '          # a b c
echo "hello" | tr -d 'l'           # heo (delete)
tr -s ' ' < file.txt               # squeeze repeated spaces

# sort lines
sort file.txt                      # alphabetical
sort -n nums.txt                   # numeric
sort -rn nums.txt                  # reverse numeric
sort -t: -k3 -n /etc/passwd        # by 3rd field, numeric
sort -u file.txt                   # sort + unique

# uniq: filter or count adjacent duplicates
sort file.txt | uniq               # unique lines
sort file.txt | uniq -c            # count occurrences
sort file.txt | uniq -d            # only duplicates
sort file.txt | uniq -u            # only unique lines

Pipes y Redirección

Los pipes conectan stdout a stdin, construyendo pipelines potentes. > redirige stdout (>> añade), 2> redirige stderr, &> captura ambos. Los here-docs (<<EOF) alimentan cadenas multilínea; entrecomillar el delimitador ('EOF') desactiva la expansión de variables. La sustitución de proceso <(cmd) trata la salida de un comando como un archivo temporal — esencial para alimentar bucles while sin un subshell, y para comandos como diff que esperan archivos.

bash
# pipe: chain commands (stdout of one -> stdin of next)
cat file | grep "x" | sort | uniq -c | sort -rn | head

# redirect stdout
echo "hello" > file.txt            # overwrite
echo "world" >> file.txt           # append

# redirect stderr
command 2> error.log               # stderr to file
command 2>&1                       # stderr to stdout
command > all.log 2>&1             # both to same file
command &> all.log                 # both (Bash shortcut)

# stdin from file
command < input.txt

# here-string
grep "x" <<< "some text with x"

# here-document (multi-line input)
cat << EOF
Line 1
Line 2 with $VAR expansion
EOF

# quoted delimiter disables expansion
cat << 'EOF'
Literal $VAR no expansion
EOF

# process substitution
diff <(ls dir1) <(ls dir2)         # compare outputs
while read line; do echo "$line"; done < <(grep x file)
07

Operaciones de Archivo y Directorio

find — Búsqueda de Archivos

find es la herramienta de búsqueda de archivos más potente. -name coincide con globs (use -iname para insensible a mayúsculas/minúsculas); -type filtra por f/d/l; -mtime/-mmin filtran por tiempo de modificación (- = dentro de, + = más antiguo que); -size filtra por tamaño. -exec ejecuta un comando en cada resultado; {} es el nombre de archivo, \; ejecuta por archivo, + los agrupa. -delete es más seguro que -exec rm para eliminación. ¡Siempre pruebe find sin -delete primero!

bash
# search by name
find . -name "*.js"
find . -iname "*.JS"               # case-insensitive
find / -name "*.conf" 2>/dev/null  # suppress permission errors

# search by type
find . -type f -name "*.txt"       # files only
find . -type d -name src           # directories only
find . -type l                     # symlinks

# search by time and size
find . -mtime -7                   # modified < 7 days ago
find . -mtime +30                  # modified > 30 days ago
find . -mmin -60                   # modified < 60 min ago
find . -size +10M                  # larger than 10MB
find . -size -1k                   # smaller than 1KB
find . -empty                      # empty files/dirs

# act on results (-exec)
find . -name "*.log" -exec rm {} \;
find . -name "*.js" -exec wc -l {} +
find . -type f -exec chmod 644 {} \;

# safe deletion with -delete
find /tmp -type f -mtime +7 -delete

Permisos y Propiedad

Los permisos son tres tripletes: propietario, grupo, otros. Cada dígito es r(4)+w(2)+x(1), así que 755 = rwxr-xr-x. chmod cambia permisos; chown cambia propietario/grupo. La notación simbólica (u+x, g-w) es más clara para cambios incrementales. umask establece permisos predeterminadas para nuevos archivos (restado de 666 para archivos, 777 para directorios). Para servidores web, 755 para directorios y 644 para archivos es estándar; nunca use 777 en producción.

bash
# view permissions
ls -l file.txt
# -rw-r--r-- 1 user group 1024 Jan 1 10:00 file.txt
#  ^^^ ^^^ ^^^
#  owner group others

# chmod: change permissions
chmod 755 script.sh        # rwxr-xr-x
chmod 644 file.txt         # rw-r--r--
chmod +x script.sh         # add execute for all
chmod u+x,g-w file.txt     # symbolic: user +x, group -w
chmod -R 755 directory/    # recursive

# chown: change owner
chown alice file.txt
chown alice:developers file.txt
chown -R alice:developers project/

# umask: default permissions for new files
umask                      # show current (e.g. 022)
umask 077                  # new files: 600, dirs: 700

# numeric permission reference:
# 7=rwx 6=rw 5=rx 4=r 3=wx 2=w 1=x 0=none

Copiar, Mover, Eliminar y Enlazar

cp -r copia directorios recursivamente; -i pregunta antes de sobrescribir (más seguro); -p preserva timestamps y permisos. mv es tanto mover como renombrar. rm -rf es peligroso — elimina recursivamente sin preguntar; siempre verifique la ruta. Los hard links apuntan al mismo inode (mismo archivo, no pueden cruzar sistemas de archivos o enlazar directorios); los symlinks son referencias de ruta (pueden enlazar cualquier cosa, pero se rompen si el objetivo se mueve). Use ln -s para symlinks.

bash
# copy
cp file.txt backup.txt
cp -r src/ dest/            # recursive (directories)
cp -i file.txt dest/        # interactive (prompt before overwrite)
cp -u file.txt dest/        # update (only if newer)
cp -p file.txt dest/        # preserve attributes

# move / rename
mv old.txt new.txt
mv file.txt /other/dir/
mv -i file.txt dest/        # prompt before overwrite

# remove
rm file.txt
rm -r directory/            # recursive
rm -f file.txt              # force (no error if missing)
rm -rf node_modules/        # force + recursive (DANGER)

# links
ln target hardlink          # hard link (same inode)
ln -s target symlink        # symbolic link (path reference)
ln -sf target symlink       # force recreate symlink

# view link target
readlink symlink
readlink -f symlink         # canonical absolute path

Archivos y Compresión

tar agrupa archivos en un archivo; gzip/bzip2/xz lo comprimen. Los flags: c=crear, x=extraer, t=listar, f=archivo, z=gzip, j=bzip2, J=xz, v=verbose. .tar.gz es el estándar de Unix; .zip es común en Windows. bzip2 comprime mejor que gzip pero más lento; xz es el mejor pero el más lento. Use -C para extraer a un directorio específico. El flag -k preserva el original al comprimir.

bash
# tar + gzip (most common)
tar -czf archive.tar.gz dir/      # create
tar -xzf archive.tar.gz           # extract
tar -xzf archive.tar.gz -C /opt/  # extract to /opt
tar -tzf archive.tar.gz           # list contents

# tar + bzip2 (better compression)
tar -cjf archive.tar.bz2 dir/
tar -xjf archive.tar.bz2

# zip / unzip
zip -r archive.zip dir/
unzip archive.zip
unzip archive.zip -d /target

# gzip / gunzip (single files)
gzip file.txt           # -> file.txt.gz
gunzip file.txt.gz      # -> file.txt
gzip -k file.txt        # keep original

# xz (best compression)
tar -cJf archive.tar.xz dir/
tar -xJf archive.tar.xz

# mnemonic: eXtract, Create, List, File, gZip, bZip2, xz

Contenido de Archivo y Comparación

cat vuelca todo el archivo; head/tail muestran los extremos; tail -f transmite actualizaciones en vivo (esencial para logs). less es un paginador interactivo con búsqueda (/) y navegación. diff compara archivos; -u produce el formato unificado usado por patch. comm compara dos archivos ordenados y muestra líneas únicas de cada uno o comunes a ambos — útil para comparar listas. Siempre ordene las entradas antes de comm, ya que requiere archivos ordenados.

bash
# view file contents
cat file.txt               # entire file
head -n 20 file.txt        # first 20 lines
tail -n 20 file.txt        # last 20 lines
tail -f app.log            # follow (live updates)
less file.txt              # pager (q to quit, / to search)

# line/word/char count
wc file.txt                # lines words bytes
wc -l file.txt             # lines only
wc -w file.txt             # words only

# compare files
diff file1.txt file2.txt
diff -u file1.txt file2.txt    # unified format (for patches)
diff -r dir1/ dir2/            # recursive

# patch
diff -u old new > change.patch
patch < change.patch
patch -R < change.patch        # reverse

# find duplicate lines across files
comm -12 <(sort a) <(sort b)   # lines in both
comm -23 <(sort a) <(sort b)   # only in a
comm -13 <(sort a) <(sort b)   # only in b
08

Gestión de Procesos y Señales

Jobs en Segundo Plano y Control de Jobs

Añadir & ejecuta un comando en segundo plano, devolviendo inmediatamente. jobs lista jobs activos; fg/bg los mueve entre primer plano y segundo plano. Ctrl+Z suspende el job en primer plano (envía SIGTSTP). wait bloquea hasta que los jobs en segundo plano terminan — esencial para scripts que lanzan trabajo paralelo. disown elimina un job de la tabla de jobs del shell para que siga ejecutándose después de cerrar sesión (a diferencia de nohup, funciona en jobs ya en ejecución).

bash
# run in background (append &)
sleep 100 &
# [1] 12345   (job 1, PID 12345)

# list jobs
jobs
jobs -l            # with PIDs

# bring to foreground / send to background
fg                 # foreground the current job
fg %1              # foreground job 1
bg %2              # background job 2 (continue running)

# suspend a running foreground job: Ctrl+Z
# then resume in background:
bg

# wait for background jobs
sleep 5 &
wait                # wait for all background jobs
wait $!             # wait for the last background job
wait 12345          # wait for specific PID

# disown: detach from shell (survives logout)
long_task &
disown %1

kill y Señales

kill envía señales a procesos. SIGTERM (predeterminado) pide al proceso que salga gracefulmente — puede limpiar. SIGKILL (-9) es forzoso e inmediato; el proceso no puede atraparlo ni ignorarlo, por lo que puede dejar recursos en mal estado. Siempre intente SIGTERM primero, espere, luego SIGKILL solo si es necesario. killall/pkill matan por nombre. pkill -f coincide con la línea de comandos completa (más flexible). Use kill -l para listar todos los nombres de señales.

bash
# send signals to processes
kill 12345              # SIGTERM (graceful, default)
kill -15 12345          # SIGTERM explicitly
kill -9 12345           # SIGKILL (force, cannot be caught)
kill -HUP 12345         # SIGHUP (reload config)

# kill by name
killall nginx           # kill all nginx processes
pkill -f "python app.py"  # match full command line

# common signals
# SIGTERM (15) - graceful termination (default)
# SIGKILL (9)  - force kill (cannot be caught/ignored)
# SIGINT (2)   - interrupt (Ctrl+C)
# SIGHUP (1)   - hangup (often reload config)
# SIGSTOP (19) - pause (cannot be caught)
# SIGCONT (18) - resume
# SIGTSTP (20) - terminal stop (Ctrl+Z)

# list all signals
kill -l

# priority: try SIGTERM first, SIGKILL as last resort
kill 12345; sleep 2; kill -9 12345 2>/dev/null

trap — Manejo de Señales

trap registra comandos para ejecutar cuando el script recibe una señal — esencial para limpieza. La pseudo-señal EXIT se dispara en cualquier salida (normal, error o kill), lo que la hace perfecta para eliminar archivos temporales. Siempre establezca traps antes de crear los recursos que limpian. Patrón común: trap cleanup EXIT INT TERM. trap '' SIGNAL la ignora; trap - SIGNAL restaura el predeterminado. Así es como los scripts robustos aseguran limpieza incluso cuando se interrumpen.

bash
# clean up on exit (EXIT signal)
trap 'rm -f $TMPFILE' EXIT
TMPFILE=$(mktemp)
echo "working with $TMPFILE"
# when script exits (normally or via error), TMPFILE is removed

# handle Ctrl+C (SIGINT)
trap 'echo "Caught Ctrl+C, exiting"; exit 130' INT

# handle multiple signals
cleanup() {
    echo "Cleaning up..."
    rm -rf "$WORKDIR"
    exit
}
trap cleanup INT TERM EXIT

# ignore a signal
trap '' INT              # ignore Ctrl+C
trap - INT               # restore default behavior

# reload on SIGHUP
trap 'echo "reloading config"; source config.sh' HUP

# list current traps
trap -p

Inspección de Procesos

ps muestra una instantánea de procesos; top/htop muestran en tiempo real. pstree muestra la jerarquía padre-hijo. pgrep encuentra PIDs por nombre (más seguro que ps|grep). lsof -i :PORT encuentra qué proceso usa un puerto — esencial para depurar errores de 'puerto en uso'. ss (socket statistics) es el reemplazo moderno de netstat. El patrón ps aux | grep es ubicuo pero pgrep es más limpio. Use --sort para encontrar los que más recursos consumen.

bash
# list processes
ps aux                   # all processes (BSD style)
ps -ef                   # all processes (System V style)
ps -ef | grep nginx      # find nginx processes

# interactive viewer (like Task Manager)
top                      # real-time, all processes
htop                     # nicer interactive viewer (install separately)

# process tree
pstree                   # tree view
pstree -p                # with PIDs

# find process by name/port
pgrep -f "node app"      # print PIDs matching
pgrep -fl "node"         # full command line

# who's listening on a port
lsof -i :8080            # process using port 8080
ss -tlnp                 # all listening TCP ports
netstat -tlnp            # legacy alternative

# process resource usage
top -b -n 1 | head       # snapshot
ps aux --sort=-%mem | head  # top memory users

nohup, disown y tmux

nohup hace que un proceso ignore SIGHUP para que sobreviva al cierre de sesión — la salida va a nohup.out. disown logra lo mismo para un job en segundo plano ya en ejecución. setsid inicia un proceso en una nueva sesión, desvinculándolo completamente. Para trabajo interactivo de larga duración, tmux o screen son mejores: mantienen una sesión de terminal completa viva a la que puede reconectarse más tarde, así que incluso los editores de texto sobreviven a la desconexión. Así es como los sysadmins gestionan servidores remotos.

bash
# nohup: run immune to hangups (survives logout)
nohup ./long_script.sh &
# output goes to nohup.out by default
nohup ./server.sh > server.log 2>&1 &

# disown: detach an already-running job
./long_task &
disown %1

# setsid: start in a new session
setsid ./daemon.sh &

# tmux: terminal multiplexer (persistent sessions)
tmux                    # start new session
tmux new -s work        # named session
tmux ls                 # list sessions
tmux attach -t work     # reattach
# inside tmux: Ctrl+B then D to detach
# processes keep running after detach

# screen: alternative to tmux
screen -S work
screen -ls
screen -r work

# check if a process is running
pgrep -x nginx && echo "running" || echo "stopped"
09

Scripting y Temas Avanzados

Shebang y Estructura del Script

El shebang (#!) le dice al kernel qué intérprete usar. #!/usr/bin/env bash es el más portable. Un script bien estructurado comienza con 'set -euo pipefail' por seguridad, define usage() y parsea argumentos con getopts (para flags cortos) o un bucle manual (para flags largos). OPTIND rastrea el siguiente argumento; shift pasado las opciones parseadas para que $1 sea el primer argumento posicional. Esta estructura hace los scripts robustos y amigables para el usuario.

bash
#!/usr/bin/env bash
# ^ portable shebang (finds bash via PATH)
# #!/bin/bash          # absolute path (common)
# #!/usr/bin/bash       # on some systems
# #!/bin/sh             # POSIX sh (most portable)

set -euo pipefail        # strict mode (see below)

# script metadata
readonly PROGNAME=$(basename "$0")
readonly VERSION="1.0.0"

usage() {
    cat << EOF
Usage: $PROGNAME [OPTIONS] FILE
Options:
  -h, --help     Show this help
  -v, --version  Show version
  -o OUTPUT      Output file
EOF
}

# parse options with getopts
while getopts "hvo:" opt; do
    case $opt in
        h) usage; exit 0 ;;
        v) echo $VERSION; exit 0 ;;
        o) output=$OPTARG ;;
        *) usage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

Modo Estricto (set -euo pipefail)

set -e sale inmediatamente si cualquier comando falla (devuelve no-cero) — captura errores temprano. set -u trata las referencias a variables no establecidas como errores. pipefail hace que un pipeline devuelva no-cero si CUALQUIER comando en él falla (por defecto, solo importa el estado del último comando). Juntos capturan la mayoría de los bugs. Use 'cmd || true' para permitir fallos esperados, e 'if ! cmd' para verificaciones explícitas. Algunos comandos (grep, test) devuelven no-cero legítimamente, así que envuélvalos.

bash
# the 'strict mode' trio
set -e             # exit on error (any non-zero status)
set -u             # error on unset variable
set -o pipefail    # pipeline fails if any command fails
# combined: set -euo pipefail

# example: without -e, errors are silently ignored
mkdir /root/nope   # fails (permission denied)
echo "still running"  # this runs anyway!

# with set -e, the script exits at the failure
set -e
mkdir /root/nope   # script exits here
echo "never reached"

# handle expected failures
set -e
if ! grep -q "x" file; then
    echo "not found"   # grep returning 1 won't kill script
fi

# temporarily disable -e
set +e
risky_command
set -e

# || true to ignore a command's failure
mkdir -p dir || true

Técnicas de Depuración

set -x (xtrace) imprime cada comando antes de ejecutarlo — la herramienta #1 de depuración. PS4 personaliza el prompt (añadir números de línea ayuda a localizar problemas). bash -n verifica sintaxis sin ejecutar. trap ERR ejecuta un comando cuando ocurre cualquier error, perfecto para registrar dónde fueron las cosas mal. Para scripts complejos, envuelva funciones sospechosas con set -x/+x para rastrear solo esa parte. Combine con set -e para detenerse en el primer error e inspeccionar.

bash
# trace execution: print each command before running
set -x              # turn on
set +x              # turn off
bash -x script.sh   # run with tracing from the start

# verbose: print input lines as read
set -v

# combine: -xv for full detail
bash -xv script.sh

# print commands with line numbers
export PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x

# check syntax without executing
bash -n script.sh   # syntax check only

# step through interactively
bash -x script.sh 2>&1 | less

# debug a specific function
debug_func() {
    set -x
    actual_func "$@"
    set +x
}

# use trap ERR to catch errors
trap 'echo "Error on line $LINENO" >&2' ERR

Aritmética y Matemáticas

Bash solo hace aritmética de enteros con $(( )). Para coma flotante, pipee expresiones a bc (scale=N establece decimales) o use awk. bc -l carga la biblioteca matemática (sqrt, sin, cos, etc.). $RANDOM da un entero pseudo-aleatorio 0-32767; para aleatoriedad criptográfica use /dev/urandom. El comando (( )) soporta +=, -=, *=, /=, %= y ++/-- como C. Para matemáticas serias, Python o awk son mejores opciones.

bash
# integer arithmetic
echo $((5 + 3))         # 8
echo $((10 / 3))        # 3 (integer division)
echo $((10 % 3))        # 1 (modulo)
echo $((2 ** 8))        # 256 (exponent)

# floating point with bc
echo "scale=2; 10/3" | bc       # 3.33
echo "3.14 * 2" | bc            # 6.28
result=$(echo "scale=4; $a / $b" | bc)

# floating point with awk
awk 'BEGIN {printf "%.2f\n", 10/3}'   # 3.33
awk "BEGIN {print $a * $b}"

# math functions with bc -l
echo "scale=4; s(0)" | bc -l    # 0 (sine, radians)
echo "scale=4; sqrt(16)" | bc -l # 4.0000

# random numbers
echo $RANDOM              # 0-32767
echo $((RANDOM % 100))    # 0-99
head -c 4 /dev/urandom | od -An -tu4   # 32-bit random

# increment/decrement
n=5
((n++)); echo $n          # 6
((n+=10)); echo $n        # 16

Regex y Pattern Matching

Bash tiene tres sistemas de patrones: globs (*.txt, ?, [abc]) para nombres de archivo, globs extendidos (extglob: !(), @(), *()) para coincidencia más compleja, y regex ERE (=~ en [[ ]]). =~ captura grupos en BASH_REMATCH (índice 0 = coincidencia completa, 1+ = grupos). Regex usa sintaxis ERE (como grep -E). Habilite extglob para negación y alternancia potentes en patrones de nombre de archivo. Regex es una de las características más útiles de Bash para validación de entrada.

bash
# glob patterns (filename matching)
*.txt          # any .txt file
file?.txt      # single char: file1.txt, fileA.txt
file[0-9].txt  # digit: file0.txt ... file9.txt
file[!0-9].txt # NOT a digit

# extended globs (shopt -s extglob)
shopt -s extglob
echo !(*.bak)       # everything except .bak files
echo @(*.jpg|*.png) # jpg or png
echo *(foo)         # zero or more "foo"

# regex with =~ in [[ ]]
if [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
    echo "valid email"
fi

# capture groups with BASH_REMATCH
if [[ $date =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
    echo "Year: ${BASH_REMATCH[1]}"
    echo "Month: ${BASH_REMATCH[2]}"
    echo "Day: ${BASH_REMATCH[3]}"
fi

# case-insensitive regex
shopt -s nocasematch
if [[ $s =~ ^hello ]]; then echo "starts with hello"; fi
10

grep en Profundidad

Patrones grep Básicos

grep busca texto usando patrones. -i ignora mayúsculas/minúsculas; -w coincide palabras completas (previene coincidir 'errors' al buscar 'error'); -v invierte (líneas no coincidentes); -c cuenta; -n muestra números de línea; -l lista solo nombres de archivo con coincidencias; -h suprime el prefijo de nombre de archivo al buscar múltiples archivos. Por defecto, grep usa Expresiones Regulares Básicas (BRE) donde los metacaracteres necesitan escape con backslash. Use -E para Regex Extendida (sintaxis más limpia) o -F para cadenas fijas (sin regex, más rápido para búsquedas literales).

bash
# Basic search (literal string)
grep "error" logfile.txt

# Case-insensitive
grep -i "error" logfile.txt

# Whole word match
grep -w "error" logfile.txt

# Invert match (lines NOT matching)
grep -v "debug" logfile.txt

# Count matches
grep -c "error" logfile.txt

# Show line numbers
grep -n "error" logfile.txt

# Multiple files with filename prefix
grep "error" *.log

# Suppress filename prefix (single file behavior)
grep -h "error" *.log

# Only show filenames with matches
grep -l "error" *.log

grep con Regex

grep -E (o egrep) usa Expresiones Regulares Extendidas con sintaxis más limpia: +, ?, |, () funcionan sin escape. ^ y $ anclan al inicio/final de línea. [] define clases de caracteres; {} especifica repetición. -P habilita regex compatible con Perl (PCRE) con características como \d, \w, lookaheads — pero esto es específico de GNU y no portable. Para regex compleja, considere ripgrep (rg) que es más rápido y usa sintaxis tipo PCRE por defecto. Siempre entrecomille patrones para prevenir la interpretación del shell de caracteres especiales como $ y *.

bash
# Extended regex (-E or egrep)
grep -E "error|warning|fatal" logfile.txt

# Anchors: ^ start, $ end
grep -E "^ERROR:" logfile.txt      # lines starting with ERROR:
grep -E "completed$" logfile.txt   # lines ending with completed

# Character classes
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" dates.txt  # date format
grep -E "[A-Z][a-z]+" names.txt                  # Capitalized words

# Quantifiers
grep -E "ab+c" file.txt    # one or more b's
grep -E "ab*c" file.txt    # zero or more b's
grep -E "ab?c" file.txt    # zero or one b

# Alternation and grouping
grep -E "(cat|dog) food" file.txt

# PCRE (-P, GNU grep only)
grep -P "\d{3}-\d{4}" phones.txt  # \d for digits

Contexto y Control de Salida de grep

Los flags de contexto (-B, -A, -C) muestran líneas circundantes, esenciales para entender entradas de log. -o muestra solo la porción coincidente (útil para extraer URLs, números, etc.). --color resalta coincidencias en terminal. -r busca recursivamente (excluye symlinks); -R sigue symlinks. --include/--exclude/--exclude-dir filtran qué archivos buscar — invaluable para codebases grandes (siempre excluya node_modules, .git, vendor). -a fuerza que los archivos binarios se traten como texto. Para búsqueda de código, ripgrep (rg) es una alternativa moderna y más rápida con defaults sensatos.

bash
# Show context: lines before/after match
grep -B 2 "error" log.txt    # 2 lines Before
grep -A 3 "error" log.txt    # 3 lines After
grep -C 2 "error" log.txt    # 2 lines Context (both)

# Only show matched part (not full line)
grep -o "https?://[^ ]+" urls.txt

# Color highlight matches
grep --color=auto "error" log.txt

# Recursive search
grep -r "TODO" ./src/
grep -R "TODO" ./src/        # follows symlinks

# Include/exclude file patterns
grep -r --include="*.py" "import" .
grep -r --exclude="*.test.js" "TODO" .
grep -r --exclude-dir=node_modules "TODO" .

# Binary files
grep -a "pattern" binary_file  # treat as text

grep con stdin y Pipelines

grep es más potente en pipelines. El truco [n]ginx previene que grep coincida con su propio proceso: el corchete hace que el patrón no coincida con la cadena literal 'grep' en la lista de procesos. zgrep busca archivos comprimidos sin descompresión manual. pgrep es un buscador de procesos especializado (mejor que ps | grep). grep -rl lista archivos que contienen un patrón; pipear a xargs grep busca dentro de esos archivos otro patrón — una técnica común de arqueología de código. Para búsqueda interactiva de código, use ripgrep o ack que están diseñados para código fuente.

bash
# Search command output
ps aux | grep nginx
ps aux | grep "[n]ginx"  # trick: prevents matching grep itself

# Search compressed logs
zcat log.gz | grep "error"
zgrep "error" log.gz     # direct

# Chain multiple greps
cat log.txt | grep "error" | grep -v "timeout" | grep -c

# Extract and filter
grep -oE "[0-9.]+" response.txt | sort -n | uniq

# Find processes excluding grep
pgrep -f "node server.js"  # better than ps | grep

# Search command history
history | grep "git rebase"

# Find files containing pattern, then search more
grep -rl "config" . | xargs grep "database"

Estado de Salida y Scripting con grep

El estado de salida de grep lo hace ideal para scripting: 0 (coincidencia encontrada), 1 (sin coincidencia), 2 (error). -q (quiet) suprime salida para verificaciones condicionales puras. En scripts con set -e, grep devolviendo 1 (sin coincidencia) saldría del script — use '|| true' para prevenirlo. El patrón while-read procesa líneas coincidentes una a la vez. grep -c devuelve un conteo (0 si no hay coincidencias), que puede comparar numéricamente. Esta capacidad de scripting hace grep un bloque de construcción para monitoreo de logs, scripts de validación y verificaciones de CI/CD.

bash
# grep returns exit codes for scripting
# 0 = match found, 1 = no match, 2 = error

if grep -q "error" /var/log/syslog; then
    echo "Errors found!"
    # send alert, etc.
fi

# Silent check (-q), no output
grep -q "^$" file.txt && echo "has blank lines"

# Use in while loop
grep "pattern" file.txt | while read -r line; do
    echo "Found: $line"
done

# Count and branch
errors=$(grep -c "ERROR" log.txt)
if [ "$errors" -gt 10 ]; then
    echo "Too many errors: $errors"
fi

# grep with set -e (exits on non-zero)
set -e
grep "missing" file.txt || true  # prevent exit
11

sed en Profundidad

Sustitución con sed

sed (stream editor) transforma texto línea por línea. El comando s sustituye texto: s/pattern/replacement/flags. g (global) reemplaza todas las ocurrencias por línea; sin él, solo se reemplaza la primera coincidencia. Puede restringir operaciones a líneas específicas (por número, rango o patrón). -i edita archivos in situ (peligroso — siempre pruebe sin -i primero, o use -i.bak para una copia de seguridad). sed procesa cada línea independientemente. El delimitador no tiene que ser / — use s|old|new|g cuando los patrones contienen barras (p. ej., rutas de archivo).

bash
# Basic substitution (first occurrence per line)
sed 's/old/new/' file.txt

# Replace all occurrences
sed 's/old/new/g' file.txt

# Case-insensitive (GNU sed)
sed 's/old/new/gi' file.txt

# Replace Nth occurrence
sed 's/old/new/2' file.txt  # 2nd occurrence only

# Only on lines matching pattern
sed '/error/s/old/new/' file.txt

# Only on specific line numbers
sed '5s/old/new/' file.txt      # line 5 only
sed '5,10s/old/new/g' file.txt  # lines 5-10
sed '$s/old/new/' file.txt      # last line

# In-place edit (modifies file)
sed -i 's/old/new/g' file.txt
sed -i.bak 's/old/new/g' file.txt  # keeps backup

sed con Regex y Grupos de Captura

sed -E (o -r) usa Expresiones Regulares Extendidas con sintaxis más limpia (sin backslash antes de paréntesis/llaves). Los grupos de captura se referencian como \1, \2, etc. en el reemplazo. & representa la coincidencia completa. El ejemplo de reformateo de fecha muestra el poder: capturar año, mes, día por separado y reordenar. Múltiples comandos pueden encadenarse con punto y coma (s/.../.../;s/.../.../). Siempre escape caracteres especiales en patrones (., *, [, etc.) y escape / en reemplazos (o use un delimitador diferente).

bash
# Extended regex (-E or -r)
sed -E 's/[0-9]+/NUMBER/g' file.txt

# Capture groups with \1, \2 (BRE)
sed 's/\(foo\)\(bar\)/\2\1/' file.txt  # swap: foobar -> barfoo

# Capture groups with () (ERE, cleaner)
sed -E 's/(foo)(bar)/\2\1/' file.txt

# Reformat date: 2025-01-15 -> 15/01/2025
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' file.txt

# Add prefix to lines
sed 's/^/>> /' file.txt

# Add suffix
sed 's/$/ <<</' file.txt

# Remove leading/trailing whitespace
sed 's/^[ \t]*//;s/[ \t]*$//' file.txt

# Use & for full match
sed 's/[0-9]\{3\}/[&]/g' file.txt  # wrap 3 digits in brackets

Eliminar e Imprimir con sed

El comando d elimina líneas; p imprime líneas. Con -n (sin auto-impresión), solo los comandos p explícitos producen salida — esto convierte sed en una impresora selectiva (como grep). Eliminar líneas en blanco (sed '/^$/d') es una limpieza común. sed -n '5,10p' es equivalente a sed -n '5,10p' o combinaciones de head/tail. La sintaxis ~ (0~3p) imprime cada 3ª línea (extensión GNU). Recuerde: sin -n, sed imprime cada línea (posiblemente modificada); con -n, no imprime nada a menos que use p. Esta dualidad hace sed tanto editor como filtro.

bash
# Delete lines
sed '5d' file.txt              # delete line 5
sed '5,10d' file.txt           # delete lines 5-10
sed '/pattern/d' file.txt      # delete matching lines
sed '/^$/d' file.txt           # delete blank lines
sed '/^[ \t]*$/d' file.txt    # delete whitespace-only lines
sed '$d' file.txt              # delete last line

# Print only specific lines (like head/tail)
sed -n '5p' file.txt           # print line 5 only
sed -n '5,10p' file.txt        # print lines 5-10
sed -n '/pattern/p' file.txt   # print matching lines (like grep)
sed -n '5,${/pattern/p}' file.txt  # from line 5 to end

# Print with line numbers
sed '=' file.txt | sed 'N;s/\n/\t/'

# Print every Nth line
sed -n '0~3p' file.txt  # every 3rd line (GNU)

sed Multilínea y Hold Space

El hold space de sed permite operaciones multilínea — el pattern space (línea actual) puede guardarse en hold space y recuperarse más tarde. h/H copia/añade a hold; g/G copia/añade desde hold; x los intercambia. El truco de unión de líneas (:a;N;$!ba;s/\n/ /g) lee todo el archivo en pattern space luego reemplaza nuevas líneas. N añade la siguiente línea al pattern space. Estas características avanzadas hacen sed Turing-completo pero también muy críptico. Para transformaciones multilínea complejas, awk o Perl son más legibles. Use sed para ediciones simples basadas en línea; use awk para lógica basada en campos o multilínea.

bash
# Join lines (replace newline with space)
sed ':a;N;$!ba;s/\n/ /g' file.txt

# Join every 2 lines
sed 'N;s/\n/ /' file.txt

# Print paragraph (blocks separated by blank lines)
sed '/./{H;d;};x;s/\n/ /g' file.txt

# Reverse line order (tac alternative)
sed '1!G;h;$!d' file.txt

# Double-space a file
sed 'G' file.txt

# Remove last line of each paragraph
sed -n '/^$/{p;h;};/./{H;};/^$/{x;s/\n.*//;p;}' file.txt

# Hold space (h,H,g,G,x) stores lines between cycles
# h: copy pattern space to hold
# H: append pattern space to hold
# g: copy hold to pattern space
# G: append hold to pattern space
# x: exchange pattern and hold

sed en Pipelines y Scripts

sed destaca en pipelines para transformación de texto. Al sustituir variables, escape caracteres especiales (& y \) para prevenir interpretación. Múltiples flags -e aplican múltiples comandos en secuencia. -f lee comandos desde un archivo (útil para scripts complejos). La conversión de CSV a TSV muestra un uso práctico. Para edición de archivos de configuración en scripts, siempre valide el cambio (grep después de sed) y considere usar herramientas dedicadas (jq para JSON, yq para YAML). La fortaleza de sed son las ediciones simples, rápidas y basadas en línea — es un pilar del procesamiento de texto de Unix junto con grep y awk.

bash
# Clean up output in pipeline
cat messy.txt | sed 's/[ \t]*$//' | sed '/^$/d' | sort

# Extract and transform
echo "name=John;age=30" | sed 's/;/\n/g' | sed 's/=/: /'

# Modify config files in scripts
sed -i "s/PORT=8080/PORT=$NEW_PORT/" config.env

# Escape special chars for safe substitution
ESCAPED=$(printf '%s\n' "$URL" | sed 's/[&\]/\\&/g')
sed "s|URL|$ESCAPED|g" template.txt

# Multiple substitutions
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# Read commands from file
sed -f script.sed input.txt

# Convert CSV to TSV
sed 's/,/\t/g' data.csv
12

awk en Profundidad

Fundamentos de awk y Campos

awk divide automáticamente cada línea en campos ($1, $2, ..., $NF). -F establece el separador de campo de entrada; OFS el de salida. $0 es la línea completa. NR (Number of Records) es el número de línea; NF (Number of Fields) es el conteo de campos de la línea actual. awk procesa cada línea a través de pares patrón-acción: patrón { acción }. Sin patrón, la acción se ejecuta para cada línea. Sin acción, la línea se imprime. Esto hace awk ideal para extracción de datos basada en columnas de CSV, TSV, /etc/passwd y archivos de log.

bash
# Print specific fields (default delimiter: whitespace)
awk '{print $1}' file.txt        # first field
awk '{print $1, $3}' file.txt    # first and third
awk '{print $NF}' file.txt       # last field
awk '{print $(NF-1)}' file.txt   # second-to-last

# Custom field separator
awk -F: '{print $1, $7}' /etc/passwd   # colon-separated
awk -F',' '{print $2}' data.csv        # CSV
awk -F'\t' '{print $1}' data.tsv      # TSV

# Print entire line ($0)
awk '{print}' file.txt
awk '{print NR, $0}' file.txt  # with line number

# NR = record (line) number
# NF = number of fields in current record
# NR and NF are built-in variables

# Change output separator
awk 'BEGIN{OFS="|"} {print $1, $2, $3}' file.txt

Patrones y Condiciones de awk

Los patrones de awk pueden ser regex (/patrón/), comparaciones ($3 > 100), números de línea (NR == 5) o rangos (/start/,/end/). ~ y !~ aplican regex a campos específicos. Las condiciones pueden combinarse con &&, || y !. Esto hace awk un filtro potente — más expresivo que grep porque puede comparar valores de campo numérica o como cadenas. El patrón de rango (/start/,/end/) imprime todas las líneas entre dos marcadores (inclusivo). awk evalúa condiciones por línea; si es verdadera, la acción se ejecuta. Sin acción, el predeterminado es {print}.

bash
# Pattern: only process matching lines
awk '/error/ {print}' log.txt          # like grep
awk '$3 > 100' data.txt                # 3rd field > 100
awk 'NR == 5' file.txt                 # line 5 only
awk 'NR >= 10 && NR <= 20' file.txt    # lines 10-20
awk 'NF == 0' file.txt                 # empty lines
awk '$1 == "ERROR"' log.txt            # first field equals ERROR

# Range pattern: from start to end
awk '/start/,/end/' file.txt

# Combine conditions
awk '$3 > 50 && $3 < 100 {print $1, $3}' data.txt
awk '$1 == "GET" || $1 == "POST" {print}' access.log

# Negation
awk '!/debug/ {print}' log.txt  # lines NOT containing debug

# Field comparison with regex
awk '$2 ~ /^[0-9]+$/' file.txt   # 2nd field is numeric
awk '$2 !~ /pattern/' file.txt   # 2nd field doesn't match

BEGIN/END y Variables de awk

BEGIN se ejecuta antes de leer cualquier entrada (ideal para inicialización, cabeceras, setup de variables). END se ejecuta después de procesar toda la entrada (ideal para resúmenes, totales). Las variables de usuario no necesitan declaración — por defecto son 0 (numérico) o vacías (cadena). -v pasa variables externas a awk. Esto hace awk un mini lenguaje de programación para procesamiento de datos: suma, promedio, min, max, conteo — todo en una pasada. El patrón 'NR == 1 {max = $1}' inicializa max desde la primera línea, luego las líneas subsiguientes lo actualizan. Esto es mucho más eficiente que múltiples pipelines grep/sort/cut.

bash
# BEGIN: runs before processing (setup)
# END: runs after processing (summary)
awk 'BEGIN {print "Total:"} {sum += $1} END {print sum}' nums.txt

# Count lines
awk 'END {print NR " lines"}' file.txt

# Sum a column
awk '{sum += $2} END {print "Total:", sum}' sales.csv

# Average
awk '{sum += $2; count++} END {print sum/count}' data.txt

# Find max value
awk 'NR == 1 {max = $1} $1 > max {max = $1} END {print max}' nums.txt

# User-defined variables (no declaration needed)
awk '{total += $1; if ($1 > max) max = $1}
     END {print "Sum:", total, "Max:", max, "Avg:", total/NR}'

# Pass variables with -v
awk -v threshold=50 '$1 > threshold {print}' data.txt
awk -v date="$(date +%Y-%m-%d)" '{print date, $0}' file.txt

Flujo de Control de awk

awk tiene flujo de control completo: if/else, for, while y arrays asociativos (clave-valor). Esto lo convierte en un lenguaje completo de procesamiento de datos. El ejemplo de conteo de palabras (words[$1]++) es el caso de uso clásico de awk — contar ocurrencias de cada valor en una columna, luego ordenar por frecuencia. Los arrays en awk son asociativos (como diccionarios), indexados por cadena o número. for (key in array) itera claves (desordenado — pipee a través de sort). awk puede reemplazar pipelines enteros de cut, sort, uniq y wc con una única pasada eficiente. Para procesamiento de datos complejo, awk es a menudo más claro que comandos Unix encadenados.

bash
# if-else
awk '{
  if ($3 >= 90) grade = "A"
  else if ($3 >= 80) grade = "B"
  else if ($3 >= 70) grade = "C"
  else grade = "F"
  print $1, grade
}' students.txt

# for loop
awk 'BEGIN {
  for (i = 1; i <= 5; i++) print i, i*i
}'

# for-in (iterate array)
awk '{count[$1]++} END {
  for (key in count) print key, count[key]
}' access.log | sort -rnk2

# while loop
awk '{
  i = 1
  while (i <= NF) {
    print i, $i
    i++
  }
}' file.txt

# Arrays (associative)
awk '{words[$1]++} END {
  for (w in words) print words[w], w
}' file.txt | sort -rn | head

Ejemplos Prácticos de awk

Estos ejemplos prácticos muestran el poder real de awk. El conteo de frecuencia de IP es esencial para análisis de logs. El resumen de tamaño de archivo por extensión usa split() para extraer extensiones. El filtrado CSV por valor de columna reemplaza pipelines complejos grep/sed. El cálculo de percentiles demuestra las capacidades matemáticas de awk (asorti ordena índices de array). Reformatear columnas (cambiar delimitador y seleccionar campos) es una tarea ETL común. awk es la herramienta de referencia para procesamiento de texto estructurado — cuando los datos tienen columnas/campos, awk es casi siempre la elección correcta. Para JSON, use jq; para CSV, awk o csvkit.

bash
# Top 10 IP addresses from access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# Or entirely in awk:
awk '{ip[$1]++} END {for (i in ip) print ip[i], i}' access.log | sort -rn | head

# Sum file sizes by extension
ls -l | awk '{split($NF, a, "."); ext=a[2]; size[ext]+=$5}
  END {for (e in size) print e, size[e]}' | sort -rnk2

# Filter CSV by column value
awk -F',' '$3 == "active" {print $1, $2}' users.csv

# Calculate response time percentiles from log
awk '{times[NR] = $2} END {
  n = asorti(times, sorted)
  print "p50:", sorted[int(n*0.5)]
  print "p95:", sorted[int(n*0.95)]
  print "p99:", sorted[int(n*0.99)]
}' response_times.txt

# Reformat columns
awk -F',' 'BEGIN{OFS="|"} {print $1, $2, $4}' data.csv
13

find y xargs

find por Nombre y Tipo

find busca en el sistema de archivos por varios criterios. -name coincide el nombre de archivo (sensible a mayúsculas/minúsculas); -iname es insensible. -type f/d/l filtra por tipo de archivo. -path coincide la ruta completa; -regex usa regex de ruta completa. -o (o) combina condiciones; use \( \) para agrupar. -maxdepth limita la profundidad de recursión (importante para el rendimiento en sistemas de archivos grandes). find devuelve rutas; combine con -exec o xargs para actuar sobre los resultados. Siempre entrecomille patrones para prevenir la expansión glob del shell. Para búsqueda de código, ripgrep (rg) es más rápido pero find es más flexible para operaciones del sistema de archivos.

bash
# Find by name (exact)
find . -name "config.txt"

# Case-insensitive name
find . -iname "config.txt"

# Wildcard pattern
find . -name "*.py"
find . -name "test_*"

# Find by type
find . -type f -name "*.js"   # files only
find . -type d -name "node_modules"  # directories
find . -type l                # symlinks

# Find by path (full path match)
find . -path "*/src/*.test.js"

# Find by regex
find . -regex ".*\.\(py\|js\)$"

# Multiple name patterns
find . \( -name "*.py" -o -name "*.js" \)

# Max depth (don't recurse too deep)
find . -maxdepth 2 -name "*.txt"

find por Tiempo y Tamaño

La búsqueda basada en tiempo de find es esencial para limpieza y auditoría. -mtime (modificación), -atime (acceso), -ctime (cambio de metadatos) usan días; -mmin/-amin/-cmin usan minutos. - (menos que) y + (más que) prefijan el valor. El tamaño usa sufijos: c (bytes), k (KB), M (MB), G (GB). -empty encuentra archivos de longitud cero o directorios vacíos. -perm verifica permisos: coincidencia exacta (644), todos los bits establecidos (-u+x), o cualquier bit establecido (/4000). Encontrar archivos SUID (/4000) es una técnica de auditoría de seguridad. Estos criterios pueden combinarse con -a (y, predeterminado) y -o (o).

bash
# Find by modification time
find . -mtime -1    # modified in last 24 hours
find . -mtime +7    # modified more than 7 days ago
find . -mtime 7     # modified exactly 7 days ago

# Access time (-atime) and change time (-ctime)
find . -atime -1    # accessed in last 24h
find . -ctime -1    # status changed in last 24h

# Minutes instead of days
find . -mmin -30    # modified in last 30 minutes

# Find by size
find . -size +100M   # larger than 100MB
find . -size -1k     # smaller than 1KB
find . -size 10M     # exactly 10MB

# Find empty files/directories
find . -empty -type f
find . -empty -type d

# Find by permissions
find . -perm 644     # exactly 644
find . -perm -u+x    # has execute for user
find / -perm /4000   # SUID files (security audit)

find -exec y -delete

-exec ejecuta un comando en cada resultado. {} es el marcador de posición para el nombre de archivo; \; termina el comando (se ejecuta una vez por archivo); + agrupa archivos (se ejecuta una vez con todos los archivos — más eficiente). -delete elimina archivos coincidentes (más rápido que -exec rm, pero pruebe primero con -print). El patrón chmod (directorios 755, archivos 644) es una configuración común de servidor web. -exec con + es preferido para comandos que aceptan múltiples archivos (grep, ls, wc). Para eliminación, siempre ejecute con -print primero para verificar qué se eliminará, luego reemplace con -delete. -ok en lugar de -exec pide confirmación por archivo.

bash
# Execute command on each result
find . -name "*.log" -exec ls -lh {} \;

# Execute with + (batch, more efficient)
find . -name "*.log" -exec ls -lh {} +

# Delete files (DANGEROUS - test first!)
find /tmp -name "*.tmp" -type f -delete
# Safer: use -exec rm
find /tmp -name "*.tmp" -type f -exec rm -i {} \;

# Execute a complex command
find . -name "*.py" -exec wc -l {} \; | sort -rn | head

# Change permissions
find . -type d -exec chmod 755 {} \;  # dirs to 755
find . -type f -exec chmod 644 {} \;  # files to 644

# Find and grep
find . -name "*.py" -exec grep "TODO" {} +

# Print before deleting (confirmation)
find . -name "*.bak" -print -delete

Fundamentos de xargs

xargs convierte stdin en argumentos de comando. Es el puente entre la salida de find y comandos que no leen stdin. -n limita argumentos por comando; -I {} define un marcador de posición para posicionamiento personalizado. -0 (con find -print0) maneja nombres de archivo con espacios/nuevas líneas correctamente — siempre use este par por seguridad. -P habilita ejecución paralela (genial para tareas CPU-bound como conversión de imágenes). -t (trace) muestra comandos antes de ejecutar; -p pide confirmación. xargs es esencial cuando -exec es demasiado lento (un proceso por archivo) — xargs agrupa argumentos eficientemente.

bash
# Basic: take stdin and pass as arguments
echo "file1 file2 file3" | xargs rm

# One argument per line
find . -name "*.bak" | xargs rm

# Control arguments per command (-n)
echo "1 2 3 4 5" | xargs -n 2 echo  # echo 1 2; echo 3 4; echo 5

# Show commands before running (-t, verbose)
find . -name "*.log" | xargs -t rm

# Prompt before each command (-p)
find . -name "*.tmp" | xargs -p rm

# Handle filenames with spaces (-0 with find -print0)
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# Replace string (-I)
find . -name "*.py" | xargs -I {} cp {} /backup/

# Parallel execution (-P)
find . -name "*.png" | xargs -P 4 -I {} convert {} {}.thumb.png

# Limit with -L (lines per command)
cat urls.txt | xargs -L 1 curl -O

Patrones find + xargs

find + xargs es el patrón clásico de Unix para operaciones masivas de archivos. -print0 | xargs -0 es la combinación segura para nombres de archivo con espacios o caracteres especiales. El patrón grep en lote es mucho más rápido que -exec grep (un proceso grep vs muchos). xargs paralelo (-P N) acelera dramáticamente tareas CPU-bound como conversión de audio/video. El patrón de archivo (encontrar logs antiguos, hacer tar) es una técnica común de rotación de logs. Siempre use -print0/-0 por robustez — sin ello, los nombres de archivo con espacios, comillas o nuevas líneas romperán el pipeline. Esta combinación es fundamental para la administración de sistemas Unix.

bash
# Find and bulk grep (efficient)
find . -name "*.py" -print0 | xargs -0 grep -l "import django"

# Find large files and show sizes
find . -type f -size +100M -print0 | xargs -0 ls -lhS

# Find and archive
find . -name "*.log" -mtime +30 -print0 |
  xargs -0 tar -czf old_logs.tar.gz

# Find and batch rename
find . -name "*.JPG" | xargs -I {} mv {} {}.bak
# Or with rename command:
find . -name "*.JPG" -exec rename 's/\.JPG$/.jpg/' {} +

# Find empty directories and remove
find . -type d -empty -print0 | xargs -0 rmdir

# Find recently modified files and copy
find . -name "*.py" -mtime -1 -print0 |
  xargs -0 -I {} cp {} /tmp/recent/

# Parallel processing
find . -name "*.wav" -print0 |
  xargs -0 -P 8 -I {} ffmpeg -i {} {}.mp3
14

Técnicas Avanzadas de Bash

Here Document y Here String

Los here documents (<< DELIMITER) alimentan texto multilínea como stdin a un comando — útiles para generar archivos de configuración, consultas SQL o cualquier entrada multilínea. Delimitador sin entrecomillar permite expansión de variables; entrecomillado ('EOF') trata el contenido literalmente. Las here strings (<<<) alimentan una única cadena como stdin — más limpio que echo | command para casos simples. El delimitador puede ser cualquier palabra (EOF, END, DONE); la convención es mayúsculas. Los here docs son esenciales para scripts que generan archivos o interactúan con programas interactivos (mysql, psql, ssh). Los delimitadores indentados (<<-) eliminan tabs iniciales por legibilidad.

bash
# Here document: multi-line input to a command
cat << EOF
Line 1
Line 2 with $VARIABLE expansion
Line 3
EOF

# Quoted delimiter: no variable expansion
cat << 'EOF'
This $won't expand
\n stays literal
EOF

# Write to file
cat > config.txt << EOF
host=localhost
port=8080
EOF

# Here string: single string as stdin
grep "pattern" <<< "search this text"
read -r line <<< "hello world"

# With variables
name="Alice"
greeting=$(cat <<< "Hello, $name!")

# Pipe multi-line to command
mysql << EOF
USE mydb;
SELECT * FROM users;
EOF

Expansión de Parámetros

La expansión de parámetros es la manipulación de cadenas integrada de Bash — sin necesidad de sed/awk/cut para operaciones simples. :- proporciona defaults; # y ## eliminan prefijos; % y %% eliminan sufijos. Los ejemplos de extensión de archivo son extremadamente comunes: ${file##*.} obtiene la extensión, ${file%.*} obtiene el basename sin extensión. / reemplaza la primera coincidencia; // reemplaza todas. ^^ y ,, convierten mayúsculas/minúsculas (Bash 4+). Estas operaciones son más rápidas que spawnear comandos externos. Domine la expansión de parámetros para escribir scripts Bash eficientes y legibles sin subshells innecesarios.

bash
# Default values
echo ${name:-"default"}    # use "default" if name unset/empty
echo ${name:="default"}    # set name to "default" if unset/empty

# String length
echo ${#var}

# Substring extraction
var="Hello World"
echo ${var:0:5}    # Hello (start:length)
echo ${var:6}      # World (start to end)

# Remove from beginning (# shortest, ## longest)
file="archive.tar.gz"
echo ${file#*.}     # tar.gz (remove up to first .)
echo ${file##*.}    # gz (remove up to last .)

# Remove from end (% shortest, %% longest)
echo ${file%.*}     # archive.tar (remove from last .)
echo ${file%%.*}    # archive (remove from first .)

# Replace
echo ${var/World/Bash}    # Hello Bash (first match)
echo ${var//l/L}          # HeLLo WorLd (all matches)

# Case conversion (Bash 4+)
echo ${var^^}   # HELLO WORLD (uppercase)
echo ${var,,}   # hello world (lowercase)

trap y Manejo de Señales

trap registra manejadores para señales y eventos. EXIT se dispara al terminar el script (normal, error o kill) — ideal para limpieza (archivos temporales, locks). INT (Ctrl-C), TERM (kill), HUP (terminal cerrado) son señales comunes. El patrón de archivo temporal (trap 'rm -f' EXIT) asegura limpieza incluso si el script falla. DEBUG se dispara antes de cada comando — útil para rastreo. Siempre entrecomille el comando trap (las comillas simples previenen la expansión inmediata). trap es esencial para scripts robustos: sin él, los archivos temporales se acumulan y los locks pueden no liberarse en caso de fallo. Siempre limpie después de usted.

bash
# Clean up on exit (any reason)
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}
trap cleanup EXIT

# Handle specific signals
trap 'echo "Interrupted!"; cleanup' INT TERM
trap 'echo "Ctrl-C pressed"' INT

# Ignore a signal
trap '' INT  # ignore Ctrl-C

# Reset trap
trap - INT  # restore default behavior

# Common pattern: temp file cleanup
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
echo "data" > "$TMPFILE"
# ... use TMPFILE ...
# File is removed when script exits (even on error)

# Debug trap (trace execution)
trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

Depuración de Scripts Bash

set -x rastrea la ejecución (imprime cada comando con prefijo +) — la herramienta principal de depuración. set -euo pipefail (modo estricto) captura errores temprano: -e sale en no-cero, -u captura errores tipográficos en nombres de variables, pipefail hace que los pipes fallen correctamente (sin él, solo importa el código de salida del último comando). bash -n verifica sintaxis sin ejecutar. PS4 personaliza el prefijo de rastreo (mostrar file:line es muy útil). trap ERR se dispara en cualquier error (con -e), dándole contexto de número de línea. Para scripts complejos, añada set -x en secciones sospechosas en lugar de globalmente. Siempre use modo estricto en scripts de producción.

bash
# Trace execution: print each command before running
bash -x script.sh
# Or within script:
set -x   # enable tracing
set +x   # disable tracing

# Verbose: print input lines as read
bash -v script.sh

# Strict mode (recommended for all scripts)
set -euo pipefail
# -e: exit on error
# -u: error on undefined variable
# -o pipefail: pipe fails if any command fails
# -o pipefail: pipe fails if any command fails

# Print specific debug info
echo "DEBUG: var=$var" >&2  # to stderr

# Check syntax without executing
bash -n script.sh

# PS4: customize trace prompt
export PS4='+${BASH_SOURCE}:${LINENO}: '
bash -x script.sh

# Trap ERR for error handling
trap 'echo "Error on line $LINENO"' ERR

curl y wget

curl y wget son clientes HTTP esenciales. curl es para testing de APIs (soporta todos los métodos HTTP, cabeceras, JSON). -d envía datos POST; -H establece cabeceras; -X especifica método; -L sigue redirecciones; -O guarda con el nombre remoto; -s es silencioso (para scripting). wget está optimizado para descargas (reanuda con -c, recursivo con -r). Para testing de APIs REST, curl con -H 'Content-Type: application/json' y -d para el cuerpo JSON es el estándar. -w '%{http_code}' extrae solo el código de estado para scripting. Para testing complejo de APIs, considere httpie (sintaxis más simple) o postman.

bash
# Basic HTTP request
curl https://api.example.com/data
curl -O https://example.com/file.zip  # save to file

# HTTP methods
curl -X POST https://api.example.com/users
curl -X PUT -d '{"name":"Alice"}' https://api.example.com/users/1
curl -X DELETE https://api.example.com/users/1

# Headers and JSON
curl -H "Content-Type: application/json" \
     -H "Authorization: Bearer TOKEN" \
     -d '{"key":"value"}' \
     https://api.example.com/data

# Download with wget
wget https://example.com/file.zip
wget -q https://example.com/file.zip  # quiet
wget -c https://example.com/file.zip  # continue/resume

# Follow redirects
curl -L https://short.url/abc

# Save response, show headers
curl -s -o response.json -w "%{http_code}" https://api.example.com

# Download with progress bar
curl --progress-bar -O https://example.com/largefile.iso

# POST form data
curl -d "name=Alice&[email protected]" https://api.example.com/form
15

Señales y Traps

manejadores trap cleanup

trap registra manejadores para señales. EXIT es especial — se dispara al salir del shell por cualquier razón, lo que lo hace perfecto para limpieza. Siempre limpie archivos de lock, directorios temporales y procesos hijos en un trap. Señales comunes a trap: INT (Ctrl-C), TERM (kill predeterminado), HUP (terminal cerrado), EXIT.

bash
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}

# catch INT (Ctrl-C), TERM, EXIT
trap cleanup INT TERM EXIT

# normal exit also triggers EXIT trap
echo "Working..."
sleep 30
# cleanup runs automatically

# remove trap
trap - EXIT                      # disable
trap - INT TERM EXIT             # disable all

archivos temporales con limpieza

Nunca cree nombres de archivo temporales a mano con $$ o $RANDOM — rutas predecibles invitan ataques de symlink. mktemp crea un archivo/directorio con nombre único atómicamente. Empárelo con un trap en EXIT para garantizar limpieza incluso en error o interrupción. Establezca la variable de entorno TMPDIR para controlar la ubicación.

bash
TMPDIR=$(mktemp -d)
TMPFILE=$(mktemp)

cleanup() {
  rm -rf "$TMPDIR" "$TMPFILE"
}
trap cleanup EXIT

# use temp files safely
echo "data" > "$TMPFILE"
process_data < "$TMPFILE"

# mktemp is safer than $RANDOM — avoids symlink attacks
# -d creates a directory
# -p /var/tmp specifies parent
# mktemp -t myapp.XXXXXX   # use TMPDIR env

manejo de señales en scripts

Para scripts de larga duración, use una variable flag cambiada por el manejador de señales para salir del bucle principal gracefully. Esto le permite terminar la iteración actual, persistir estado y salir limpiamente. Evite hacer trabajo pesado dentro del manejador trap — solo establezca flags.

bash
#!/bin/bash
# graceful shutdown for a long-running script

RUNNING=1
stop() {
  echo "Stopping..."
  RUNNING=0
}
trap stop INT TERM

while [ $RUNNING -eq 1 ]; do
  echo "Tick $(date)"
  sleep 1
done

echo "Done."

# send signals: kill -TERM <pid> or Ctrl-C

timeout y watch

timeout ejecuta un comando y lo mata (SIGTERM por defecto) después de una duración. El código de salida 124 significa que se agotó el tiempo. Use --kill-after para escalar a SIGKILL si el proceso ignora SIGTERM. Combine con bucles para construir health checks. watch es para visualización periódica orientada a humanos, no para scripts.

bash
# timeout kills a command after N seconds
timeout 30 slow_command
timeout 5s curl https://slow.example.com
timeout --signal=KILL 60 build.sh  # escalate to SIGKILL
timeout -k 5 30 command            # kill -9 after 5s grace

# exit status: 124 if timed out
timeout 2 sleep 10
echo $?                            # 124

# watch reruns a command
watch -n 1 date
watch -n 5 'kubectl get pods'

# tlimit-style loops
while true; do
  timeout 5 curl -s https://api.example.com/health || echo "down"
  sleep 60
done

ignorar y reenviar

trap '' SIGNAL ignora una señal (útil para secciones críticas no interrumpibles). Para reenviar señales a un hijo, capture su PID con $! y reenvíe. El trap ERR se dispara en cualquier fallo de comando cuando set -e está habilitado — útil para registrar el número de línea fallida vía $LINENO.

bash
# ignore a signal entirely
trap '' INT                       # ignore Ctrl-C
trap - INT                        # restore default

# re-raise / propagate to children
trap 'kill -TERM $childpid; wait $childpid' TERM

# forward signals to a subprocess
childpid=
run_child() {
  some_server &
  childpid=$!
  wait $childpid
}
trap 'kill -TERM $childpid' TERM INT
run_child

# exit on any error
set -e
trap 'echo "Failed at line $LINENO"' ERR
16

Funciones y Bibliotecas

definir funciones

Las funciones agrupan comandos reutilizables. Dos sintaxis: name() {...} y function name {...}. Los argumentos dentro de funciones son $1, $2, etc. — $0 sigue siendo el nombre del script. $@ expande a todos los args (siempre entrecomille como "$@" para preservar args con espacios). $# es el conteo.

bash
# two equivalent syntaxes
greet() {
  echo "Hello, $1!"
}

function greet {                  # also valid
  echo "Hello, $1!"
}

greet Alice                       # Hello, Alice!
greet "Bob Smith"                 # quoted args

# arguments: $1, $2, ... $@, $#, $*
echo_args() {
  echo "Count: $#"
  echo "All: $@"
  echo "First: $1"
  for arg in "$@"; do echo "- $arg"; done
}

valores de retorno y locales

return establece el estado de salida (0=éxito, 1-255=fallo) — NO devuelve un valor. Para obtener un valor de vuelta, haga echo y capture con $(). Use local para variables dentro de funciones para evitar contaminar el scope global. local es esencial en funciones recursivas.

bash
# return is exit status (0-255), not a value
is_even() {
  if (( $1 % 2 == 0 )); then return 0; else return 1; fi
}

if is_even 4; then echo "even"; fi

# capture stdout as a value
get_date() { date +%Y-%m-%d; }
today=$(get_date)

# local variables
counter() {
  local count=0                  # scoped to function
  ((count++))
  echo $count
}

hacer source de bibliotecas

source (o .) ejecuta un archivo en el shell actual — así las funciones, variables y aliases se vuelven disponibles. Así es como construye bibliotecas reutilizables. BASH_SOURCE[0] vs $0 distingue sourced vs ejecutado — útil para archivos que son tanto biblioteca como script ejecutable.

bash
# library file: lib/utils.sh
log() {
  echo "[$(date +%H:%M:%S)] $*" >&2
}

CONFIG_PATH="/etc/myapp"

# main script
source lib/utils.sh              # or: . lib/utils.sh
log "Starting..."
echo "Config: $CONFIG_PATH"

# check if sourced vs executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "Running directly"
else
  echo "Sourced"
fi

funciones recursivas

Bash soporta recursión, pero es lento (cada llamada spawnea subshells para $()) y tiene profundidad de pila limitada (~1000s). Para trabajo intensivo de cómputo, prefiera awk, Python o herramientas externas. Siempre use variables locales en funciones recursivas — de lo contrario se pisan entre sí entre llamadas.

bash
# factorial
factorial() {
  local n=$1
  if (( n <= 1 )); then
    echo 1
  else
    local prev=$(factorial $((n - 1)))
    echo $((n * prev))
  fi
}
factorial 5                      # 120

# fibonacci
fib() {
  if (( $1 < 2 )); then echo $1; return; fi
  echo $(( $(fib $(($1 - 1))) + $(fib $(($1 - 2))) ))
}

# caution: bash recursion is slow and limited depth
# for heavy work, use awk, python, or external tools

argumentos por defecto y opcionales

${1:-default} sustituye un default si $1 está sin establecer o vacío. ${1:?message} sale con un error si falta — genial para args requeridos. shift saca $1 de la lista de argumentos. Combinar case + shift es la forma idiomática de parsear flags y args posicionales en funciones de bash.

bash
# default values
greet() {
  local name="${1:-World}"        # default if unset or empty
  local greeting="${2:-Hello}"
  echo "$greeting, $name!"
}
greet                            # Hello, World!
greet Alice                      # Hello, Alice!
greet Alice Hi                   # Hi, Alice!

# required argument
require_arg() {
  : "${1:?Usage: require_arg <name>}"   # exits with message if missing
  echo "Got: $1"
}

# shift through args
process() {
  while (( $# )); do
    case "$1" in
      -v) verbose=1; shift ;;
      *)  files+=("$1"); shift ;;
    esac
  done
}
17

Arrays y Arrays Asociativos

arrays indexados

Los arrays indexados usan claves enteras basadas en 0. Siempre entrecomille "${arr[@]}" al iterar para manejar correctamente elementos con espacios. ${#arr[@]} es el conteo. ${!arr[@]} da los índices (útil para arrays dispersos). unset 'arr[i]' (entrecomillado) elimina un elemento.

bash
# declare and assign
fruits=("apple" "banana" "cherry")
fruits+=("date")                 # append
fruits[10]="kiwi"                # sparse

# access
echo "${fruits[0]}"             # first
echo "${fruits[@]}"             # all values
echo "${#fruits[@]}"            # count
echo "${!fruits[@]}"            # indices

# iterate
for f in "${fruits[@]}"; do
  echo "$f"
done

# slice
echo "${fruits[@]:1:2}"         # 2 items starting at index 1

# delete
unset 'fruits[1]'

arrays asociativos (mapas)

Los arrays asociativos (declare -A) son una característica de bash 4+ (macOS incluye bash 3 por defecto — use brew install bash). Las claves son cadenas. El test -v verifica si existe una clave. Itere claves con ${!arr[@]}. El /bin/bash predeterminado de macOS es 3.2 — los scripts que usan -A necesitan #!/usr/bin/env bash apuntando a un bash más nuevo.

bash
# requires bash 4+
declare -A ages
ages["alice"]=30
ages["bob"]=25
ages["carol"]=42

# access
echo "${ages["alice"]}"
echo "${ages[@]}"               # all values
echo "${!ages[@]}"              # all keys

# iterate keys
for name in "${!ages[@]}"; do
  echo "$name => ${ages[$name]}"
done

# check key exists
if [[ -v ages["alice"] ]]; then echo "yes"; fi

# count
echo "${#ages[@]}"

# delete
unset 'ages[bob]'

leer en arrays

mapfile (también conocido como readarray) es la forma más rápida de cargar líneas en un array — siempre use -t para eliminar las nuevas líneas finales. El bucle while-read es portable pero más lento. Para dividir una cadena delimitada, establezca IFS y use read -ra. < <(cmd) la sustitución de proceso evita problemas de scope de variables del subshell.

bash
# read lines into array
mapfile -t lines < file.txt      # bash 4+
echo "${lines[0]}"

# alternative (portable)
lines=()
while IFS= read -r line; do
  lines+=("$line")
done < file.txt

# split string into array
IFS=',' read -ra csv <<< "a,b,c,d"
echo "${csv[2]}"                # c

# from command
mapfile -t files < <(find . -name "*.js")

# process each
for f in "${files[@]}"; do
  echo "Processing $f"
done

operaciones de array

Los arrays de Bash carecen de muchas operaciones integradas (no .indexOf, .reverse, .unique). Para estas, escriba bucles o pipee a través de sort/uniq. El truco de array único usa un array asociativo como set. Para manipulación compleja de arrays, considere awk o un lenguaje de programación real — los arrays de bash son mejores para listas simples.

bash
# append
arr=(a b c)
arr+=(d e)                       # (a b c d e)

# length
echo "${#arr[@]}"

# slice
echo "${arr[@]:1:2}"            # (b c)

# find index (no built-in; loop)
idx=-1
for i in "${!arr[@]}"; do
  if [[ "${arr[$i]}" == "b" ]]; then idx=$i; break; fi
done

# reverse
n=${#arr[@]}
for ((i=n-1; i>=0; i--)); do
  echo "${arr[$i]}"
done

# unique (preserve order)
declare -A seen
unique=()
for x in "${arr[@]}"; do
  [[ -v seen[$x] ]] && continue
  seen[$x]=1
  unique+=("$x")
done

datos anidados con arrays

Bash no tiene arrays anidados nativos. Workarounds: (1) almacene registros como cadenas unidas por delimitador y divida con IFS, (2) use un array asociativo con claves compuestas como "r,c", (3) use indirección de variable con ${!name}. Para datos anidados reales, cambie a jq + JSON o un lenguaje real.

bash
# array of "records" (delimiter-based)
users=("alice:30:admin" "bob:25:user")
for u in "${users[@]}"; do
  IFS=':' read -r name age role <<< "$u"
  echo "$name ($age) - $role"
done

# simulate 2D with name-mangled vars
declare -A grid
grid["0,0"]=1; grid["0,1"]=2
grid["1,0"]=3; grid["1,1"]=4
for key in "${!grid[@]}"; do
  IFS=',' read -r r c <<< "$key"
  echo "[$r,$c] = ${grid[$key]}"
done

# arrays of arrays (not directly supported)
# workaround: use indirection
rows=("row0" "row1")
row0=(a b c)
row1=(d e f)
ref="${rows[0]}[@]"
echo "${!ref}"                   # a b c
18

Manipulación de Cadenas

expansión de parámetros

La expansión de parámetros es la herramienta de cadena más potente de bash. # y ## eliminan del frente (coincidencia más corta/más larga); % y %% eliminan del final. / reemplaza la primera, // reemplaza todas. Estas evitan spawnear subshells — mucho más rápido que pipear a través de sed para operaciones simples.

bash
path="/var/log/app.log"

# length
echo "${#path}"                 # 16

# substring (offset:length)
echo "${path:5}"                # log/app.log
echo "${path:5:3}"              # log

# remove from front (shortest/longest)
echo "${path#*/}"               # var/log/app.log
echo "${path##*/}"              # app.log (basename)

# remove from end
echo "${path%/*}"               # /var/log (dirname)
echo "${path%%.*}"              # /var/log/app

# replace
echo "${path/log/LOG}"          # /var/LOG/app.log (first)
echo "${path//log/LOG}"         # all occurrences

conversión de mayúsculas/minúsculas

Bash 4+ añadió ,, (minúsculas), ^^ (mayúsculas), ^ (capitalizar primer carácter), , (minúsculas primer carácter). declare -u/-l autoconvierte asignaciones. Para bash más antiguo (predeterminado de macOS), use tr o awk. La conversión de mayúsculas/minúsculas es común al normalizar entrada de usuario o construir identificadores.

bash
s="Hello World"

# bash 4+
echo "${s,,}"                   # hello world (lower)
echo "${s^^}"                   # HELLO WORLD (upper)
echo "${s^}"                    # Hello World (capitalize first)
echo "${s,}"                    # hello World (lowercase first)

# pattern-based
echo "${s^^[aeiou]}"            # hEllO wOrld (vowels upper)

# portable (pre-bash 4)
echo "$s" | tr 'a-z' 'A-Z'
echo "$s" | awk '{print toupper($0)}'

# declare typed variables
declare -u UPPER="hello"         # HELLO
declare -l LOWER="WORLD"         # world

dividir y unir

Dividir: establezca IFS y use read -ra. Unir: ${arr[*]} con IFS establecido funciona pero es delicado — la función join_by es más fiable. El truco printf '%s' "$d%s" antepone el delimitador a cada arg restante, produciendo una unión limpia. tr puede convertir entre delimitadores y nuevas líneas.

bash
# split on delimiter
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo "${parts[2]}"              # c
for p in "${parts[@]}"; do echo "$p"; done

# join with delimiter (bash 4+)
arr=(a b c d)
joined=$(IFS=,; echo "${arr[*]}")
echo "$joined"                   # a,b,c,d

# custom joiner
join_by() {
  local d=$1; shift
  local first=$1; shift
  printf '%s' "$first"
  printf '%s' "$d%s" "$@"
}
join_by " | " a b c              # a | b | c

# split into lines
tr ',' '\n' <<< "a,b,c"

búsqueda y test

Pattern matching: == con wildcards (* ? [..]) hace coincidencia glob. =~ hace regex extendida; los grupos de captura van a BASH_REMATCH. shopt -s nocasematch hace las comparaciones [[ ]] insensibles a mayúsculas/minúsculas. expr es legacy — prefiera [[ ]] para código nuevo.

bash
s="Hello World"

# substring search
if [[ "$s" == *"World"* ]]; then echo "contains"; fi
if [[ "$s" == Hello* ]]; then echo "starts with"; fi
if [[ "$s" == *World ]]; then echo "ends with"; fi

# regex match
if [[ "$s" =~ ^H[a-z]+\sW[a-z]+$ ]]; then echo "match"; fi
echo "${BASH_REMATCH[0]}"       # whole match
echo "${BASH_REMATCH[1]}"       # first group

# case-insensitive
shopt -s nocasematch
[[ "$s" == *world* ]] && echo "yes"
shopt -u nocasematch

# index (no built-in; use expr)
expr "$s" : ".*World"            # length up to match

formateo printf

printf es más potente que echo — soporta especificadores de formato estilo C (%s, %d, %f, %x), anchura/precisión y alineación. %()T formatea tiempo sin spawnear el comando date. printf -v almacena el resultado en una variable. printf '=%.0s' {1..40} es un truco ingenioso para repetir un carácter N veces.

bash
# basic
printf '%s\n' "hello" "world"   # one per line
printf '%s = %d\n' "count" 42

# width and padding
printf '%10s\n' "right"         # right-aligned, width 10
printf '%-10s|\n' "left"        # left-aligned
printf '%05d\n' 42              # 00042 (zero-padded)
printf '%.2f\n' 3.14159         # 3.14

# date formatting
printf 'Today: %(%Y-%m-%d)T\n' -1

# to variable
printf -v today '%(%Y%m%d)T' -1
echo "$today"                    # 20260621

# repeat
printf '=%.0s' {1..40}; echo     # 40 equals signs
19

Expresiones Regulares

regex de bash con =~

=~ usa ERE (regex extendida). Los grupos de captura pueblan BASH_REMATCH (índice 0 = coincidencia completa, 1+ = grupos). Entrecomille la cadena pero NO la regex (entrecomillar la regex la hace literal). Para coincidencia insensible a mayúsculas/minúsculas, use shopt -s nocasematch. Regex es POSIX ERE — no \d, \w; use [0-9], [A-Za-z0-9_].

bash
s="user_42"

if [[ "$s" =~ ^([a-z]+)_([0-9]+)$ ]]; then
  echo "Match!"
  echo "Name: ${BASH_REMATCH[1]}"   # user
  echo "ID: ${BASH_REMATCH[2]}"     # 42
fi

# email-ish check
email="[email protected]"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "valid"
fi

# case-insensitive
shopt -s nocasematch
[[ "$s" =~ USER ]] && echo "yes"
shopt -u nocasematch

patrones regex de grep

grep por defecto a BRE (regex básica) donde +, ?, |, () necesitan backslashes. -E habilita ERE (sintaxis más limpia). -P habilita PCRE con \d, \w, \b, lookarounds — más potente pero menos portable (solo GNU grep). Para scripts que deben ejecutarse en macOS/BSD, prefiera -E.

bash
# basic (BRE) — default
grep 'foo' file
grep 'a\.' file                 # literal dot (escape in BRE)
grep 'a*b' file                  # zero or more a

# extended (ERE) — -E
grep -E 'a+b' file               # one or more
grep -E 'a|b' file               # alternation
grep -E '(ab)+' file             # grouping
grep -E '[0-9]{3}' file          # quantifier

# perl (PCRE) — -P
grep -P '\d{3}' file            # digits
grep -P '\bword\b' file        # word boundary
grep -P '(?<=foo)bar' file       # lookbehind

# anchors
grep -E '^Error' file            # line start
grep -E 'done$' file             # line end
grep -E '^Error.*timeout$' file

sustitución regex de sed

sed usa BRE por defecto (los grupos de captura necesitan \( \), backrefs \1). -E cambia a ERE (() y +, ?, | más limpios). El flag I hace la sustitución insensible a mayúsculas/minúsculas (GNU sed). Use un delimitador diferente (s|...|...|) cuando el patrón contiene barras. [[:space:]] es portable para espacios en blanco.

bash
# BRE (default)
sed 's/foo/bar/' file
sed 's/a\.b/x/' file            # literal dot
sed 's/a\(b\)c/\1/' file      # capture with backrefs

# ERE with -E
sed -E 's/a+b/x/' file
sed -E 's/(foo|bar)/X/g' file
sed -E 's/([0-9]{4})/Year: \1/' file

# case-insensitive
sed 's/foo/bar/I' file
sed 's/[fF][oO][oO]/bar/g' file

# common recipes
sed -E 's/^[[:space:]]+//' file  # trim leading whitespace
sed -E 's/[[:space:]]+$//' file  # trim trailing
sed -E 's/ +/ /g' file           # collapse spaces
sed -E 's#https?://[^ ]+##g' file # strip URLs

regex de awk

awk usa ERE. El operador ~ prueba una cadena contra una regex. /pat1/, /pat2/ es un patrón de rango (coincide desde pat1 hasta pat2 inclusivo). gsub hace sustitución global in-place. match() de gawk con un tercer arg captura grupos en un array — útil pero no portable a mawk/POSIX awk.

bash
# match operator ~
awk '$1 ~ /^[0-9]+/' file        # field 1 is numeric
awk '$0 !~ /debug/' file         # line doesn't contain
awk '/^Error/,/^$/' file         # from Error to blank line

# case-insensitive
awk 'BEGIN { IGNORECASE=1 } /error/' file
awk 'tolower($0) ~ /error/' file

# gsub (global substitute)
awk '{ gsub(/foo/, "bar"); print }' file
awk '{ gsub(/[0-9]+/, "N"); print }' file

# match with capture (gawk)
awk 'match($0, /id=([0-9]+)/, m) { print m[1] }' file

# split on regex
awk '{ n = split($0, parts, /[,;\t]/); print n }' file

recetas regex comunes

Estas recetas cubren tareas comunes de extracción de texto. -o imprime solo la porción coincidente. Note que regex no es ideal para parsear HTML, JSON o XML — use parsers propios (jq para JSON, xmllint para XML). Para IPv4, esta regex coincide formato pero no rangos válidos (255.255.255.255 vs 999.999.999.999).

bash
# IPv4
grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log

# URL
grep -Eo 'https?://[^[:space:]]+' file

# email
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file

# date YYYY-MM-DD
grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' file

# hex color
grep -Eo '#[0-9A-Fa-f]{6}' file

# strong password (8+, upper, lower, digit)
grep -E '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,}$' file

# trim whitespace
sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' file

# extract domain from URL
sed -E 's#https?://([^/]+).*#\1#' file
20

Depuración

opciones de set

set -e sale en cualquier fallo de comando (imprescindible para scripts). set -u captura errores tipográficos en nombres de variables. set -o pipefail hace que un pipe falle si cualquier parte falla (sin él, solo importa el código de salida del último comando). set -x rastrea la ejecución. Combinado como -euo pipefail, esto es 'modo estricto' — el default más seguro.

bash
#!/bin/bash
set -e                           # exit on error
set -u                           # error on unset variable
set -o pipefail                  # pipe fails if any command fails
set -x                           # print commands (trace)
# or combined:
set -euo pipefail

# shorthand
set -Eeuxo pipefail              # -E: ERR trap inherits

# strict mode for robust scripts
set -euo pipefail
IFS=$'\n\t'

# trace specific section
set -x
complex_logic
set +x

# debug a script
bash -x script.sh
bash -xv script.sh               # verbose + trace

rastreo con -x

set -x imprime cada comando antes de ejecutarlo, con + como prefijo predeterminado (PS4). Personalizar PS4 para incluir file:line:function hace los rastros mucho más útiles para depuración. BASH_XTRACEFD redirige el rastro a un FD diferente, para que pueda separar la salida de rastreo de stdout/stderr.

bash
# trace entire script
bash -x script.sh

# trace inside script
set -x
echo "tracing on"
set +x

# customize PS4 (the trace prompt)
export PS4='+ ${BASH_SOURCE:-shell}:${LINENO}: ${FUNCNAME[0]:-main}() '
set -x
my_function
set +x

# trace to file
exec 5> /tmp/trace.log
BASH_XTRACEFD=5
set -x
# ... commands ...
set +x
exec 5>&-

manejo de errores y trap ERR

El trap ERR se dispara cuando un comando falla (con set -e). $? dentro del trap da el código de salida fallido. $LINENO muestra dónde ocurrió el fallo. El builtin caller imprime file:line:function — iterarlo produce un stack trace. set -E hace que el trap ERR se dispare también dentro de funciones (de lo contrario se desactiva en contexto de función).

bash
set -e
set -E                           # ERR trap inherits functions

on_error() {
  local exit_code=$?
  local line=$1
  echo "ERROR: command failed at line $line with exit code $exit_code" >&2
  echo "Stack:" >&2
  local i=0
  while caller $i >&2; do ((i++)); done
  exit $exit_code
}
trap 'on_error $LINENO' ERR

# now any failing command triggers the handler
false                            # triggers on_error

# caller builtin gives file:line:func
# stack trace via: while caller $i; do ((i++)); done

linter shellcheck

shellcheck es el linter de facto para scripts de shell — captura problemas de comillado, vars sin usar, errores comunes y sugiere idioms. Ejecútelo en cada script. La corrección más común es SC2086: entrecomille variables para prevenir word-splitting y globbing. Muchos pipelines de CI requieren que shellcheck pase.

bash
# install: apt install shellcheck (or brew, or use shellcheck.net)
# run:
shellcheck script.sh
shellcheck -x script.sh          # trust includes (no SC1090 warnings)
shellcheck -S warning script.sh  # only warnings+
shellcheck --exclude=SC2086 *.sh # exclude specific rules

# common warnings:
# SC2086: double-quote to prevent globbing (use "$var")
# SC2046: quote $(cmd) to prevent word splitting
# SC2034: unused variable
# SC2155: declare and assign separately (masks return code)
# SC2004: $/${} unnecessary in arithmetic

# in script: disable inline
# shellcheck disable=SC2086
echo $unquoted_var

# disable for next line
# shellcheck disable=SC2086
echo $args

logging y diagnósticos

Un logger con niveles (DEBUG/INFO/WARN/ERROR) con timestamps y colores hace los scripts mucho más fáciles de depurar. Siempre loguee a stderr para que stdout se mantenga limpio para datos. Soporte un flag --verbose (habilita set -x) y --dry-run (imprime comandos sin ejecutar). Códigos de color ANSI: 31=rojo, 33=amarillo, 32=verde, 90=gris.

bash
# logging function with levels
log() {
  local level=$1; shift
  local msg="$*"
  local ts=$(date '+%Y-%m-%d %H:%M:%S')
  local color
  case $level in
    DEBUG) color='\033[90m' ;;
    INFO)  color='\033[32m' ;;
    WARN)  color='\033[33m' ;;
    ERROR) color='\033[31m' ;;
  esac
  printf "%b[%s] [%s] %s\033[0m\n" "$color" "$ts" "$level" "$msg" >&2
}

log INFO "Starting process"
log WARN "Disk space low"
log ERROR "Connection failed"

# verbose flag
[[ $VERBOSE ]] && set -x

# dry-run mode
if [[ $DRY_RUN ]]; then
  echo "Would: $cmd"
else
  eval "$cmd"
fi
21

Seguridad

comillado e inyección

La regla #1 de seguridad en bash: entrecomille cada expansión de variable. $var sin entrecomillar sufre word-splitting y globbing, llevando a bugs e inyección. Para comandos con args dinámicos, use un array (cmd=(ls "$dir"); "${cmd[@]}") en lugar de una cadena. Nunca haga eval o $() entrada de usuario en una cadena de shell.

bash
# ALWAYS quote variables
file="my file.txt"
rm $file                         # BAD: rm my file.txt (3 args!)
rm "$file"                       # GOOD: rm "my file.txt"

# use arrays for command + args
files=("a.txt" "b file.txt" "c.txt")
rm "${files[@]}"                # each file as separate arg, safely

# avoid eval
eval "echo $user_input"          # DANGEROUS
echo "$user_input"               # safe

# avoid command substitution in unsafe places
# BAD: filename from user, executed
cmd="ls $userdir"
$cmd                             # word-splitting + globbing

# GOOD: use array
cmd=(ls "$userdir")
"${cmd[@]}"

archivos temporales seguros y secretos

mktemp crea archivos con nombres impredecibles y permisos 600 — seguros de races de symlink. Siempre limpie con un trap. Lea secretos de variables de entorno, nunca hardcode. Evite pasar secretos como args de línea de comandos (visibles vía ps) — use stdin, variables de entorno o un archivo de configuración con permisos 600.

bash
# safe temp files
tmp=$(mktemp)                    # atomic, unpredictable name
trap 'rm -f "$tmp"' EXIT

# safe temp dir
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

# secrets: read from env, never hardcode
api_key="${API_KEY:?Set API_KEY env var}"

# don't leak secrets to process list (ps)
# BAD: curl -H "Authorization: Bearer $TOKEN" ...
# GOOD: pass via stdin or config file
curl -H @- <<EOF https://api.example.com
Authorization: Bearer $TOKEN
EOF

# restrict file perms for secrets
umask 077
echo "$api_key" > ~/.secrets/api
chmod 600 ~/.secrets/api

validación de entrada

Valide toda entrada de usuario antes de usarla. Regex verifica formato; aritmética verifica rangos. Para rutas de archivo, rechace .. (traversal) y rutas absolutas a menos que se permita explícitamente. tr -cd elimina caracteres no permitidos — útil para sanitizar identificadores. Whitelist (case) es más seguro que blacklist para acciones permitidas.

bash
# validate numeric input
read -p "Enter a number: " n
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
  echo "Not a number" >&2; exit 1
fi

# validate range
if (( n < 1 || n > 100 )); then
  echo "Out of range" >&2; exit 1
fi

# validate file path (no traversal)
path="$1"
case "$path" in
  *..*|/*) echo "Invalid path" >&2; exit 1 ;;
esac

# sanitize filename
safe=$(echo "$name" | tr -cd 'A-Za-z0-9._-')

# whitelist allowed values
case "$action" in
  start|stop|restart) ;;
  *) echo "Invalid action" >&2; exit 1 ;;
esac

caída de privilegios y setuid

Los scripts de shell setuid son inseguros — la mayoría de sistemas ignoran el bit setuid en scripts. Use sudo con una entrada sudoers estricta (comando específico, sin contraseña) en su lugar. Caiga de root lo antes posible vía exec sudo -u. Para capacidades (p. ej., enlazar puerto 80), use setcap en lugar de ejecutar como root. chmod 700 mantiene los scripts privados al propietario.

bash
# drop privileges in a script running as root
if [[ $EUID -eq 0 ]]; then
  exec sudo -u appuser "$0" "$@"
fi

# check for root
if [[ $EUID -ne 0 ]]; then
  echo "Requires root" >&2; exit 1
fi

# setuid scripts are DANGEROUS — avoid them
# instead, use sudoers entries
# /etc/sudoers.d/myapp:
#   appuser ALL=(root) NOPASSWD: /usr/local/bin/myapp.sh

# restrict script permissions
chmod 700 script.sh              # only owner can run
chown appuser:appuser script.sh

# use capsh instead of full root for fine-grained caps
# setcap 'cap_net_bind_service=+ep' /usr/bin/myapp

firmas e integridad

Siempre verifique checksums y firmas para scripts descargados, especialmente instaladores ejecutados como root. sha256sum -c compara contra un hash conocido bueno. Las firmas GPG prueban tanto integridad como autenticidad (la identidad del firmante). Pipear curl a sha256sum le permite inspeccionar el hash antes de ejecutar — pero el patrón más seguro es descargar-luego-verificar-luego-ejecutar.

bash
# verify checksum
sha256sum -c file.sha256
# file.sha256 contains: <hash>  <filename>

# generate checksums
sha256sum release.tar.gz > release.tar.gz.sha256
sha256sum *.iso > isos.sha256

# GPG sign and verify
gpg --sign file.txt              # creates file.txt.gpg
gpg --verify file.txt.gpg
gpg --detach-sig --armor file.tar.gz
gpg --verify file.tar.gz.asc file.tar.gz

# download + verify in one go
curl -fsSL https://example.com/install.sh | sha256sum
curl -fsSL https://example.com/install.sh -o install.sh
sha256sum -c <(echo "expected_hash  install.sh")
22

Redes

curl Avanzado

curl transfiere datos vía URLs. -X establece método, -d envía datos, -H establece cabeceras. -O guarda con el nombre remoto. -I obtiene solo cabeceras. -L sigue redirecciones. -u user:pass para auth. -v para verbose.

bash
# POST with data
curl -X POST -d "name=Alice" https://api.example.com
# JSON POST
curl -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' https://api.example.com
# Download with progress
curl -O https://example.com/file.zip
# Headers only
curl -I https://example.com

wget

wget descarga archivos de forma no interactiva. -m hace mirror de sitios. -c reanuda descargas interrumpidas. -r recursa, -l limita profundidad. -b ejecuta en segundo plano. Mejor que curl para descargas recursivas.

bash
# Download file
wget https://example.com/file.zip
# Mirror website
wget -m https://example.com
# Resume download
wget -c https://example.com/largefile.zip
# Recursive download
wget -r -l 2 https://example.com
# Background
wget -b https://example.com/file.zip

ssh y scp

ssh conecta a máquinas remotas. -i especifica un archivo de clave. -L crea forward de puerto local. scp copia archivos sobre SSH. -r recursa directorios. Use ssh-copy-id para instalar claves. Configure ~/.ssh/config para aliases.

bash
# SSH connect
ssh user@host
# SSH with key
ssh -i ~/.ssh/id_rsa user@host
# Port forwarding
ssh -L 8080:localhost:80 user@host
# Copy file
scp file.txt user@host:/path/
# Copy directory
scp -r dir/ user@host:/path/

netstat y ss

netstat y ss muestran conexiones de red. -t TCP, -u UDP, -l escuchando, -n numérico, -p proceso. ss es más rápido y más detallado que netstat. Use para encontrar qué proceso usa un puerto o diagnosticar problemas de conexión.

bash
# List listening ports
netstat -tlnp
ss -tlnp
# Show all connections
netstat -an
ss -tunap
# Show routing table
netstat -r
# Show interface stats
netstat -i

ping y traceroute

ping prueba alcanzabilidad y latencia (-c limita el conteo). traceroute muestra la ruta a un host. dig consulta registros DNS (A, MX, NS, TXT). +short da salida concisa. Use para diagnósticos de red y troubleshooting de DNS.

bash
# Check connectivity
ping -c 4 google.com
# Trace route
traceroute google.com
# DNS lookup
dig example.com
dig +short example.com
# Get DNS records
dig MX example.com
dig NS example.com
23

Shell Scripting Profundo

Funciones

Las funciones agrupan comandos. local crea variables de scope de función. $1, $2 son argumentos. return establece estado de salida (0-255). Capture salida con $(). Las funciones deben definirse antes de usar. Use source para cargar desde archivos.

bash
greet() {
    local name="$1"
    echo "Hello, $name"
    return 0
}
greet "Alice"
# Return value via $?
result=$(greet "Bob")

Condicionales

[ ] es el comando test. -f archivo, -d directorio, -z cadena vacía, -n no vacía. = para cadenas, -eq/-ne/-gt/-lt para números. Siempre entrecomille variables para manejar espacios. [[ ]] es bash mejorado con regex.

bash
if [ -f "/path/file" ]; then
    echo "File exists"
elif [ -d "/path" ]; then
    echo "Directory exists"
else
    echo "Not found"
fi
# String comparison
if [ "$str" = "hello" ]; then ... fi
# Numeric
if [ "$num" -gt 10 ]; then ... fi

Bucles

for itera sobre listas. for estilo C usa (( )). while lee hasta que la condición falle. while read procesa archivos línea por línea de forma segura. Siempre entrecomille variables. Use find para iteración recursiva de archivos.

bash
# For loop
for i in 1 2 3; do
    echo $i
done
# C-style
for ((i=0; i<5; i++)); do echo $i; done
# While loop
while read line; do
    echo "$line"
done < file.txt
# Iterate files
for f in *.txt; do process "$f"; done

Arrays

Los arrays usan paréntesis. ${arr[@]} expande todos los elementos. ${#arr[@]} es la longitud. Siempre entrecomille "${arr[@]}" para manejar espacios. declare -A crea arrays asociativos (bash 4+). Use read -a para dividir en array.

bash
# Declare array
fruits=("apple" "banana" "cherry")
# Access
echo ${fruits[0]}      # apple
echo ${fruits[@]}      # all
echo ${#fruits[@]}     # length
# Iterate
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done
# Associative array
declare -A config
config[key]="value"

Manejo de Errores

set -e sale en error, -u en vars no definidas, pipefail captura fallos de pipe. trap ERR maneja errores. trap EXIT ejecuta limpieza. command -v verifica si un comando existe. Siempre comience scripts con set -euo pipefail.

bash
set -e  # Exit on error
set -u  # Error on undefined variable
set -o pipefail  # Pipeline fails if any command fails
set -euo pipefail  # All three (recommended)
trap 'echo "Error on line $LINENO"' ERR
trap 'cleanup' EXIT
# Check command result
if ! command -v docker &> /dev/null; then
    echo "docker not installed"
fi
24

Operaciones de Archivo Profundo

find Avanzado

find busca archivos por criterios. -type f/d para archivos/directorios. -size +1M más grande que 1MB. -mtime -7 modificado dentro de 7 días. -exec ejecuta un comando en cada coincidencia. {} es el nombre de archivo; \; termina el comando.

bash
# By name
find . -name "*.txt"
# By type and size
find . -type f -size +1M
# By modification time
find . -mtime -7  # Modified in last 7 days
# Execute command
find . -name "*.log" -exec gzip {} \;
# Delete
find . -name "*.tmp" -delete

tar y Compresión

tar combina archivos; -z comprime con gzip. -c crear, -x extraer, -t listar, -v verbose, -f nombre de archivo. Para .tar.bz2 use -j. zip/unzip manejan formato ZIP. Use -C para extraer a un directorio específico.

bash
# Create tar.gz
tar -czvf archive.tar.gz dir/
# Extract
tar -xzvf archive.tar.gz
# List contents
tar -tzvf archive.tar.gz
# zip/unzip
zip -r archive.zip dir/
unzip archive.zip

rsync

rsync sincroniza archivos eficientemente, transfiriendo solo diferencias. -a modo archivo (preserva atributos), -v verbose, -z comprimir. --delete elimina archivos no en el origen. La barra diagonal final en src/ importa: con ella, copia contenidos; sin ella, copia el directorio.

bash
# Sync local
rsync -av src/ dest/
# Sync to remote
rsync -avz src/ user@host:/path/
# Delete files not in source
rsync -av --delete src/ dest/
# Dry run
rsync -av --dry-run src/ dest/
# Exclude
rsync -av --exclude='*.log' src/ dest/

permisos de archivo

Permisos: r=4, w=2, x=1. 755 = rwxr-xr-x (propietario completo, otros lectura/ejecución). 644 = rw-r--r-- (archivos). u/g/o/a = usuario/grupo/otros/todos. chmod -R recursa. chown cambia la propiedad.

bash
# Symbolic
chmod u+x file      # User execute
chmod g-w file      # Group write off
chmod a=r file      # All read only
# Numeric
chmod 755 file      # rwxr-xr-x
chmod 644 file      # rw-r--r--
# Change owner
chown user:group file
# Recursive
chmod -R 755 dir/

ln y symlinks

Los hard links apuntan al mismo inode (solo mismo sistema de archivos). Los enlaces simbólicos (-s) apuntan a una ruta. Los hard links sobreviven a la eliminación del original. Los symlinks se rompen si el objetivo se mueve. Use readlink para resolver symlinks.

bash
# Hard link
ln file.txt hardlink.txt
# Symbolic link
ln -s /path/to/target symlink
# Find broken symlinks
find . -type l ! -exec test -e {} \; -print
# Copy symlink target
cp -L symlink dest/
25

Errores Comunes

Variables Sin Entrecomillar

Las variables sin entrecomillar se dividen en espacios y glob. $(ls) se rompe con nombres de archivo que contienen espacios. Use globs (*.txt) o find -print0 con read -d. Siempre entrecomille variables: "$var". Use IFS= read -r para lectura segura de líneas.

bash
# BUG: breaks with spaces
for f in $(ls); do echo "$f"; done
# GOOD: handle spaces
for f in *.txt; do echo "$f"; done
# Or with find
find . -name "*.txt" -print0 | while IFS= read -r -d '' f; do
    echo "$f"
done

cp vs mv

cp copia, mv mueve. Ambos pueden sobrescribir sin advertencia. Use -i para preguntar antes de sobrescribir. Use -n para nunca sobrescribir. mv es atómico en el mismo sistema de archivos, útil para locks y actualizaciones atómicas.

bash
# cp: copy (original remains)
cp file.txt backup.txt
# mv: move/rename (original removed)
mv file.txt newname.txt
# Common mistake: overwriting
cp important.txt /tmp/  # If /tmp/important.txt exists, overwritten!
# Use -i for safety
cp -i file.txt /tmp/

Peligros de rm

rm -rf es peligroso, especialmente con variables. Las variables vacías causan rm -rf /. Siempre entrecomille y verifique variables. Considere trash-cli para eliminación recuperable. Nunca ejecute rm -rf / o con sudo descuidadamente.

bash
# DANGEROUS: recursive force
rm -rf /  # Never do this!
rm -rf $VAR/  # If $VAR is empty, becomes rm -rf /
# Safer: check first
if [ -n "$VAR" ]; then
    rm -rf "$VAR/"
fi
# Use trash-cli instead
trash-put file.txt

Shebang

El shebang (#!) especifica el intérprete. #!/usr/bin/env bash es más portable que #!/bin/bash. Use /bin/sh para máxima portabilidad (evita bashisms). Siempre haga los scripts ejecutables con chmod +x.

bash
#!/bin/bash          # bash script
#!/usr/bin/env bash  # Portable bash
#!/usr/bin/env python3  # Python
#!/bin/sh            # POSIX sh (more portable)
# Make executable
chmod +x script.sh
./script.sh

Estado de Salida

$? contiene el estado de salida del último comando. 0 = éxito, no-cero = fallo. Verifique inmediatamente antes de que otro comando lo sobrescriba. Mejor usar if comando directamente. Siempre salga con códigos de estado significativos en scripts.

bash
# Check last command
if [ $? -eq 0 ]; then
    echo "Success"
fi
# Better: direct check
if command; then
    echo "Success"
else
    echo "Failed"
fi
# Exit with status
exit 0  # Success
exit 1  # Failure

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.