En la lección anterior aprendiste a moverte por srv-veloz-01 y quedó una pregunta en el aire: por qué cd no puede ser un programa externo. Esa pregunta es la puerta de entrada a esta lección. Hasta ahora has usado el shell; ahora vas a entender qué ocurre exactamente entre que pulsas Enter y aparece el resultado. Este conocimiento no es teórico: explica por qué una variable definida en una tubería desaparece, por qué which a veces miente, por qué tu script "no encuentra" un comando que tú sí ves, y por qué source y ejecutar un fichero no son lo mismo. Son precisamente los errores que más tiempo hacen perder a quien programa en Bash sin este modelo mental.

Contenido

  1. Qué ocurre cuando pulsas Enter
  2. Los tipos de comando y cómo distinguirlos
  3. El PATH y el orden de búsqueda
  4. Procesos padre e hijo: fork y exec
  5. Subshells y por qué las variables no sobreviven
  6. Entorno frente a variables de shell
  7. $?: el código de salida
  8. Poniéndolo todo junto en Veloz Envíos

  1. Qué ocurre cuando pulsas Enter

Escribes ls -lh /var/log/veloz y pulsas Enter. Lo que parece instantáneo son en realidad seis fases bien definidas.

graph TD
    A["Pulsas Enter"] --> B["1. Lectura y troceado<br/>divide la línea en palabras"]
    B --> C["2. Expansiones<br/>~, $VAR, *, $(...), aritmética"]
    C --> D["3. Resolución del comando<br/>alias → keyword → función → builtin → PATH"]
    D --> E{"¿Es interno?"}
    E -->|"Sí: builtin/función"| F["Ejecuta dentro del propio Bash"]
    E -->|"No: ejecutable externo"| G["4. fork(): crea proceso hijo"]
    G --> H["5. exec(): el hijo se convierte<br/>en el programa"]
    H --> I["Bash espera con wait()"]
    F --> J["6. Código de salida en $?"]
    I --> J
    J --> K["Nuevo prompt"]

Veamos cada fase.

Fase 1: lectura y troceado

Bash lee la línea completa y la divide en palabras usando espacios, tabuladores y saltos de línea como separadores. También identifica los operadores especiales (|, >, &&, ;). De ls -lh /var/log/veloz salen tres palabras.

Aquí ya aparece la primera fuente de errores. Si un nombre de fichero contiene espacios, el troceado lo parte en dos:

ls mi informe.txt

Bash entiende dos argumentos, no uno. Por eso las comillas son importantes: ls "mi informe.txt". Todo el detalle está en 03-06.

Fase 2: expansiones

Bash reescribe la línea antes de ejecutar nada. Sustituye:

Expansión Ejemplo Se convierte en
Llaves log-{a,b}.txt log-a.txt log-b.txt
Virgulilla ~/veloz-ops /home/joan/veloz-ops
Parámetros $HOME /home/joan
Comandos $(date +%F) 2026-08-03
Aritmética $((2 + 3)) 5
Rutas (globbing) *.log acceso.log app.log

Este punto es crucial y sorprende a mucha gente: el comando nunca ve los asteriscos. Cuando escribes ls *.log, quien expande el patrón es Bash, y ls recibe ya la lista de nombres. Puedes comprobarlo:

cd /var/log/veloz
echo *.log
acceso.log app.log veloz-api.log

echo no sabe nada de comodines: recibió tres argumentos ya resueltos. El detalle completo de las expansiones se trata en 02-05 y 03-06; aquí basta con que sepas que ocurren antes.

Fase 3: resolución del comando

Bash toma la primera palabra y busca en un orden estricto qué es. Lo veremos en la sección 2, porque es donde se concentran las sorpresas.

Fase 4 y 5: fork y exec

Si el comando resulta ser un ejecutable externo, Bash no puede "convertirse" en él (dejaría de existir tu shell). Hace dos cosas:

  1. fork(): crea una copia de sí mismo, un proceso hijo.
  2. exec(): el hijo reemplaza su propia imagen en memoria por la del programa a ejecutar.

El padre (tu Bash) se queda esperando con wait() a que el hijo termine. Por eso el prompt no vuelve hasta que el comando acaba, salvo que lo lances en segundo plano con &.

Fase 6: código de salida

Cuando el hijo muere, devuelve un número entre 0 y 255. Bash lo guarda en $?. Es la base de todo el control de errores en scripting, y lo veremos en la sección 7.

  1. Los tipos de comando y cómo distinguirlos

No todo lo que escribes en el prompt es un programa. Bash reconoce cinco categorías, y las busca en este orden exacto:

Orden Tipo Qué es Ejemplos
1 Alias Sustitución de texto definida por ti ll, ops
2 Keyword Palabra reservada de la sintaxis de Bash if, for, while, function, [[
3 Función Bloque de código que has definido Tus funciones de lib/
4 Builtin Comando integrado en el propio Bash cd, echo, export, source, type
5 Ejecutable externo Un fichero encontrado en el PATH ls, grep, awk, date

El orden importa. Si defines una función llamada ls, se usará en lugar del /usr/bin/ls. Y si además defines un alias ls, el alias gana a la función.

2.1 type: la herramienta fiable

type es un builtin de Bash y responde exactamente lo que Bash haría.

type cd
type ls
type if
type echo
cd es una orden interna del shell
ls es un alias de «ls --color=auto»
if es una palabra clave del shell
echo es una orden interna del shell

La opción -a muestra todas las coincidencias, en orden de prioridad. Es reveladora:

type -a echo
echo es una orden interna del shell
echo es /usr/bin/echo
echo es /bin/echo

Existen dos echo: el builtin de Bash y el programa /usr/bin/echo de coreutils. Gana el builtin, siempre. Y no son idénticos: sus opciones difieren ligeramente entre sistemas, lo que provoca scripts que funcionan en una máquina y no en otra. Por eso en scripting profesional se prefiere printf, que es mucho más predecible (lo veremos en el Módulo 3).

Otro ejemplo muy ilustrativo con nuestro PATH de veloz-ops:

type -a bash
bash es /usr/bin/bash
bash es /bin/bash

Aparecen dos rutas porque en los sistemas modernos /bin es un enlace simbólico a /usr/bin: es el mismo fichero visto de dos formas.

2.2 command -v: la versión para scripts

command -v ls
command -v cd
alias ls='ls --color=auto'
cd

command -v imprime la ruta (o la definición) y devuelve un código de salida que indica si existe o no. Es la forma estándar y portable de comprobar disponibilidad en un script:

if command -v jq > /dev/null 2>&1; then
    echo "jq disponible"
else
    echo "Falta jq: instala con sudo apt install jq"
fi

Este patrón lo usaremos de verdad en el Módulo 6, cuando veloz-ops necesite jq para hablar con la API.

2.3 which: por qué no debes fiarte

which ls
which cd
/usr/bin/ls

Observa: which cd no imprime nada. Y ahí está el problema. which es un programa externo (en muchos sistemas, un script) que se limita a recorrer el PATH buscando ficheros. No sabe nada de builtins, funciones ni keywords, porque no puede ver el interior de tu shell.

Herramienta Tipo Ve builtins Ve funciones Ve alias Portable
type Builtin de Bash Bash/ksh/zsh
command -v Builtin POSIX Sí, POSIX
which Programa externo No No Depende Inconsistente

La conclusión práctica: usa type -a para investigar de forma interactiva y command -v dentro de los scripts. Evita which. Un caso real de confusión: un compañero se queja de que time no existe porque which time no devuelve nada; en realidad time es una keyword de Bash, y type time lo aclara al instante.

  1. El PATH y el orden de búsqueda

Cuando Bash llega al paso 5 (ejecutable externo), recorre los directorios de PATH de izquierda a derecha y ejecuta la primera coincidencia.

echo "$PATH" | tr ':' '\n'
/home/joan/veloz-ops/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin

Puntos importantes:

  • La búsqueda se detiene en la primera coincidencia. Si hubiera un ls en ~/veloz-ops/bin, se ejecutaría ese y no el del sistema.
  • El orden lo decides tú. Poner tu directorio delante da prioridad a tus herramientas; ponerlo detrás es más seguro.
  • Bash cachea las rutas encontradas para no repetir la búsqueda. Si instalas un programa nuevo y Bash sigue diciendo que no existe, limpia la caché:
hash -r

O consulta qué tiene memorizado:

hash
aciertos	comando
   4	/usr/bin/ls
   2	/usr/bin/date

Este es un problema real y frustrante: acabas de mover un script de sitio, lo ejecutas y Bash intenta lanzarlo desde la ruta antigua. hash -r lo resuelve en un segundo.

3.1 Por qué ./script.sh y no script.sh

Si estás en ~/veloz-ops/bin y escribes informe.sh, Bash buscará en el PATH. Como el directorio actual no está en el PATH (y por seguridad no debe estarlo, según vimos en 01-02), no lo encontrará:

bash: informe.sh: orden no encontrada

La solución es indicar una ruta explícita: ./informe.sh. En cuanto una palabra contiene una barra /, Bash deja de buscar en el PATH y la trata como ruta directa.

Con nuestra configuración de veloz-ops hay un matiz agradable: como añadimos ~/veloz-ops/bin al PATH, los scripts que pongamos ahí se podrán invocar por su nombre desde cualquier directorio, igual que ls. Es exactamente el efecto que buscábamos al montar el toolkit.

  1. Procesos padre e hijo: fork y exec

Cada proceso en Linux tiene un identificador (PID) y un padre (PPID). Tu shell también.

echo "Mi PID es $$"
echo "Mi padre es $PPID"
Mi PID es 3412
Mi padre es 3399

$$ es el PID del shell actual y $PPID el de su padre (normalmente el emulador de terminal o sshd).

Comprobemos la relación padre-hijo en directo:

echo "Shell actual: $$"
bash -c 'echo "Shell hijo: $$, su padre: $PPID"'
Shell actual: 3412
Shell hijo: 3587, su padre: 3412

El hijo tiene un PID distinto y su padre es tu shell. Esa es la mecánica del fork.

Cuando el hijo termina, todo lo que hizo en su propia memoria desaparece: variables, directorio actual, funciones. Solo sobrevive lo que escribió en disco o envió por su salida estándar. Este principio explica la mitad de los "misterios" de Bash.

4.1 La excepción: exec sin fork

Si usas exec explícitamente, Bash no crea un hijo: se reemplaza a sí mismo por el programa.

exec date

Se muestra la fecha y la terminal se cierra, porque tu shell ya no existe: se convirtió en date, y date terminó. Es una herramienta especializada, muy usada en los entrypoint.sh de contenedores Docker para que el proceso de la aplicación herede el PID 1 y reciba correctamente las señales de parada. No la uses a la ligera.

  1. Subshells y por qué las variables no sobreviven

Un subshell es un proceso hijo que también es un Bash. Se crean en más situaciones de las que la gente sospecha.

Construcción ¿Crea subshell? Ejemplo
( comandos ) (cd /tmp; ls)
Tubería ` ` , para cada lado (en Bash por defecto)
Sustitución $( ) hoy=$(date +%F)
Segundo plano & tarea &
{ comandos; } No, agrupa en el shell actual { cd /tmp; ls; }
source fichero No source ~/.bashrc
./script.sh Ejecuta un fichero

5.1 El experimento clave

contador=0
echo "Antes: $contador"
( contador=99; echo "Dentro del subshell: $contador" )
echo "Después: $contador"
Antes: 0
Dentro del subshell: 99
Después: 0

Los paréntesis crearon un proceso hijo con una copia de las variables. Ese hijo modificó su copia y murió. El padre nunca se enteró. No es un fallo: es cómo funcionan los procesos en Unix. La información fluye del padre al hijo, nunca al revés.

Compara con las llaves:

contador=0
{ contador=99; echo "Dentro de las llaves: $contador"; }
echo "Después: $contador"
Dentro de las llaves: 99
Después: 99

Las llaves solo agrupan; no hay proceso nuevo. (Atención a la sintaxis: las llaves necesitan espacios interiores y un ; antes de la de cierre.)

5.2 El caso que arruina scripts de verdad

Este es el error clásico, y lo planteamos con datos de Veloz Envíos. Queremos contar cuántas líneas de app.log son errores:

errores=0
grep 'ERROR' /var/log/veloz/app.log | while read -r linea; do
    errores=$((errores + 1))
done
echo "Errores encontrados: $errores"
Errores encontrados: 0

Cero, pese a que sí hay errores en el fichero. ¿Por qué? Porque el lado derecho de la tubería se ejecuta en un subshell. El bucle incrementó correctamente su propia copia de errores hasta, digamos, 47; después el subshell terminó y la copia se evaporó. El echo final lee la variable del shell padre, que sigue valiendo 0.

Hay tres soluciones, y conviene conocerlas todas:

# Solución 1: redirección de entrada en lugar de tubería
errores=0
while read -r linea; do
    errores=$((errores + 1))
done < <(grep 'ERROR' /var/log/veloz/app.log)
echo "Errores encontrados: $errores"
Errores encontrados: 47

La construcción < <(...) se llama sustitución de procesos: el grep se ejecuta aparte, pero el bucle while corre en el shell principal, así que la variable sobrevive. Se estudia a fondo en 05-05.

# Solución 2: activar la opción lastpipe (solo Bash, requiere job control desactivado)
shopt -s lastpipe
errores=0
grep 'ERROR' /var/log/veloz/app.log | while read -r linea; do
    errores=$((errores + 1))
done
echo "Errores encontrados: $errores"

Con lastpipe, el último comando de la tubería se ejecuta en el shell actual. Funciona en scripts, pero no en sesiones interactivas normales.

# Solución 3: no usar el shell para contar
errores=$(grep -c 'ERROR' /var/log/veloz/app.log)
echo "Errores encontrados: $errores"
Errores encontrados: 47

Esta es la mejor: grep -c cuenta por sí mismo y la sustitución de comandos captura su salida. Cuando existe una herramienta que ya hace el trabajo, no lo hagas con un bucle. Es más rápido, más corto y no tiene el problema del subshell.

5.3 El mismo principio explica source

Ahora se entiende del todo lo que vimos en 01-02:

# fichero: config.sh
CIUDAD="Valencia"
./config.sh          # ejecuta en un subshell
echo "$CIUDAD"       # vacío
source config.sh     # ejecuta en el shell actual
echo "$CIUDAD"       # Valencia
Valencia

Y por eso cd tiene que ser un builtin: si fuera un programa externo, se ejecutaría en un hijo, cambiaría el directorio de ese hijo y moriría. Tu directorio no se movería un milímetro. Aquella pregunta pendiente de la lección anterior queda resuelta.

  1. Entorno frente a variables de shell

Hay dos clases de variables, y la diferencia es exactamente la de la sección anterior: qué se hereda.

  • Variable de shell: existe solo en el shell actual. No se pasa a los hijos.
  • Variable de entorno: se copia al entorno de cada proceso hijo.
local_var="solo aquí"
export global_var="viaja a los hijos"

bash -c 'echo "local_var=[$local_var] global_var=[$global_var]"'
local_var=[] global_var=[viaja a los hijos]

El hijo no vio local_var porque nunca se exportó.

6.1 Herramientas para inspeccionar

Comando Qué muestra
env Variables de entorno (las exportadas)
printenv Igual que env; admite printenv NOMBRE
set Todas las variables y funciones del shell actual
declare -p Variables con su tipo y atributos
export -p Solo las exportadas, con la sintaxis declare -x
printenv HOME PATH USER
/home/joan
/home/joan/veloz-ops/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
joan
export -p | grep -i veloz
declare -x PATH="/home/joan/veloz-ops/bin:/usr/local/sbin:..."
declare -x VELOZ_ENV="produccion"

Variables de entorno que te encontrarás siempre:

Variable Contenido
HOME Tu carpeta personal
USER Tu nombre de usuario
PATH Directorios de búsqueda de comandos
PWD Directorio actual
OLDPWD Directorio anterior (lo que usa cd -)
SHELL Shell de login configurado
LANG Idioma y codificación
TERM Tipo de terminal
EDITOR Editor preferido

6.2 Ejecutar con un entorno modificado

Puedes definir una variable solo para un comando, sin tocar tu shell:

VELOZ_ENV=pruebas bash -c 'echo "Entorno: $VELOZ_ENV"'
echo "En mi shell sigue siendo: [$VELOZ_ENV]"
Entorno: pruebas
En mi shell sigue siendo: []

Esta sintaxis —asignación delante del comando, sin ;— es limpísima y muy usada en la práctica, por ejemplo para forzar un idioma en la salida de un comando:

LC_ALL=C date
Mon Aug  3 10:15:22 CEST 2026

Con LC_ALL=C la fecha sale en inglés. Es un truco importante en scripts: si tu script analiza la salida de un comando, fuerza LC_ALL=C para que no dependa del idioma del servidor. Un grep que busca "ago" fallaría en una máquina configurada en inglés.

Y para ejecutar algo con un entorno completamente limpio:

env -i bash -c 'echo "PATH=[$PATH] HOME=[$HOME]"'
PATH=[] HOME=[]

env -i borra todo el entorno. Es la mejor forma de simular las condiciones de cron, que ejecuta con un entorno mínimo. Si tu script funciona así, funcionará en cron. Lo aplicaremos en 07-01.

  1. $?: el código de salida

Todo comando devuelve al terminar un número entre 0 y 255:

  • 0 significa éxito.
  • Cualquier otro valor significa error.

Es al revés de lo que sugiere la intuición, pero tiene sentido: hay una única forma de acertar y muchas de fallar, así que cada valor distinto de cero puede identificar un tipo de fallo.

ls /var/log/veloz > /dev/null
echo "Código: $?"

ls /directorio/inexistente 2> /dev/null
echo "Código: $?"
Código: 0
Código: 2

Códigos con significado convencional:

Código Significado
0 Éxito
1 Error genérico
2 Uso incorrecto del comando (argumentos mal)
126 El fichero existe pero no es ejecutable
127 Comando no encontrado
130 Interrumpido con Ctrl+C (128 + señal 2)
137 Terminado con kill -9 (128 + señal 9)

Comprobémoslos:

comando_que_no_existe
echo "Código: $?"
bash: comando_que_no_existe: orden no encontrada
Código: 127

Un detalle que causa errores sutiles: $? se sobrescribe con cada comando, incluido el echo que la muestra.

ls /inexistente 2> /dev/null
echo "Primera lectura: $?"
echo "Segunda lectura: $?"
Primera lectura: 2
Segunda lectura: 0

La segunda lectura devuelve 0 porque es el resultado del echo anterior, que funcionó bien. Si necesitas el valor más de una vez, guárdalo inmediatamente:

ls /inexistente 2> /dev/null
codigo=$?
echo "Guardado: $codigo"
echo "Sigue disponible: $codigo"
Guardado: 2
Sigue disponible: 2

Un caso especial que merece atención: en una tubería, $? es el código del último comando, no del primero.

grep 'ERROR' /fichero/inexistente | wc -l
echo "Código: $?"
grep: /fichero/inexistente: No existe el archivo o el directorio
0
Código: 0

Aunque grep falló, wc -l funcionó, así que la tubería declara éxito. Es una fuente de errores silenciosos en scripts. Se resuelve con set -o pipefail o consultando el array PIPESTATUS, temas de la lección 05-03.

Los códigos de salida son la base de &&, ||, if y del control de errores en general. Aquí solo era un primer contacto; el uso completo llega en 03-03 y 03-04.

  1. Poniéndolo todo junto en Veloz Envíos

Un escenario realista que combina todo lo anterior. Un compañero te dice: "He escrito un script veloz-estado que cuenta los errores del log, pero devuelve siempre 0 y encima cuando lo lanzo desde cron dice que no encuentra el comando."

Diagnostiquemos paso a paso.

Paso 1: ¿qué es realmente veloz-estado?

type -a veloz-estado
veloz-estado es /home/joan/veloz-ops/bin/veloz-estado

Existe y está en el PATH. Pero cron no lee tu ~/.bashrc (lo vimos en 01-02), así que su PATH no incluye ~/veloz-ops/bin. Simulemos el entorno de cron:

env -i /bin/sh -c 'veloz-estado'
/bin/sh: 1: veloz-estado: not found

Reproducido. La solución: usar la ruta absoluta en el crontab, o definir el PATH dentro del propio script.

Paso 2: ¿por qué cuenta 0?

El script contiene esto:

total=0
grep 'ERROR' /var/log/veloz/app.log | while read -r linea; do
    total=$((total + 1))
done
echo "Total de errores: $total"

Ya sabes el diagnóstico: el while corre en un subshell y total no sobrevive. La corrección:

total=$(grep -c 'ERROR' /var/log/veloz/app.log)
echo "Total de errores: $total"

Paso 3: ¿y si el fichero no existe?

total=$(grep -c 'ERROR' /var/log/veloz/app.log)
codigo=$?
echo "Total: $total (código $codigo)"

Si el log no existe, grep devuelve 2 y total queda vacío. Guardar el código permite reaccionar. En 03-04 aprenderás a convertir eso en una condición, y en 05-03 a hacerlo de forma robusta.

Paso 4: verificación final del entorno

echo "PID del shell: $$"
echo "Comando resuelto: $(command -v veloz-estado)"
echo "Directorio: $PWD"
VELOZ_ENV=produccion veloz-estado
echo "Salida del script: $?"

Este pequeño ritual —saber qué se va a ejecutar, con qué entorno y qué devolvió— es exactamente lo que distingue a alguien que depura con método de alguien que prueba cosas al azar.

Errores Comunes y Consejos

  • Usar which para comprobar si un comando existe. No ve builtins ni funciones y su comportamiento varía entre sistemas. Usa command -v en scripts y type -a de forma interactiva.
  • Esperar que una variable modificada dentro de una tubería sobreviva. El lado derecho de | es un subshell. Usa < <(...), lastpipe, o mejor aún, una herramienta que ya haga el cálculo (grep -c, wc -l, awk).
  • Ejecutar un fichero de configuración con ./config.sh esperando que defina variables. Necesitas source config.sh.
  • Olvidar export y extrañarse de que el script hijo no vea la variable. Sin export, la variable no sale del shell actual.
  • Leer $? demasiado tarde. Cualquier comando intermedio la sobrescribe, incluido un echo. Guárdala en cuanto la necesites.
  • Suponer que una tubería falla si falla el primer comando. $? refleja el último. Usa set -o pipefail (05-03).
  • Modificar un script, moverlo y que Bash siga ejecutando la versión antigua. Es la caché de rutas: hash -r.
  • Dar por hecho que el entorno de cron es el tuyo. No lo es. Prueba tus scripts con env -i antes de programarlos.
  • Consejo: cuando algo se comporte de forma inexplicable, hazte estas tres preguntas por orden: ¿qué es realmente este comando (type -a)?, ¿se está ejecutando en un subshell?, ¿qué variables hay en el entorno (env)?. Cubren la mayoría de los casos.
  • Consejo: bash -x script.sh muestra cada línea tras las expansiones, antes de ejecutarla. Es la forma más rápida de ver el proceso de la sección 1 en acción. Lo formalizaremos en 05-03.

Ejercicios

Ejercicio 1: Clasificar comandos

Sin ejecutar nada, predice qué tipo de comando es cada uno (alias, keyword, función, builtin o externo) y luego verifica con type -a:

  1. cd
  2. grep
  3. [[
  4. export
  5. for
  6. awk

Explica además por qué which cd no devuelve nada.

Ejercicio 2: El contador que no cuenta

Este script pretende contar cuántos envíos con estado incidencia hay en /srv/veloz/datos/envios.csv, pero siempre imprime 0:

#!/usr/bin/env bash
incidencias=0
cat /srv/veloz/datos/envios.csv | while IFS=',' read -r id fecha ciudad repartidor estado importe; do
    if [ "$estado" = "incidencia" ]; then
        incidencias=$((incidencias + 1))
    fi
done
echo "Incidencias: $incidencias"
  1. Explica exactamente por qué falla.
  2. Corrígelo de dos formas distintas.
  3. Indica cuál de las dos preferirías en veloz-ops y por qué.

Ejercicio 3: Entorno y códigos de salida

Responde ejecutando lo necesario:

  1. Define una variable CIUDAD="Bilbao" sin exportar y comprueba si un bash -c hijo la ve. Repite exportándola.
  2. Ejecuta un comando que devuelva el código 127 y otro que devuelva el 2, y muestra ambos códigos.
  3. Ejecuta grep 'ERROR' /fichero/que/no/existe | wc -l y explica por qué $? vale 0.
  4. Comprueba si jq está instalado en tu sistema usando command -v dentro de un if.

Soluciones

Solución al Ejercicio 1

type -a cd grep '[[' export for awk
cd es una orden interna del shell
grep es /usr/bin/grep
[[ es una palabra clave del shell
export es una orden interna del shell
for es una palabra clave del shell
awk es /usr/bin/awk
Comando Tipo Razón
cd Builtin Debe cambiar el directorio del shell actual
grep Externo Programa independiente en /usr/bin
[[ Keyword Parte de la sintaxis; Bash la analiza de forma especial (por eso no hay división de palabras dentro)
export Builtin Modifica el entorno del shell actual
for Keyword Estructura de control del lenguaje
awk Externo Es un lenguaje completo con su propio intérprete

which cd no devuelve nada porque which es un programa externo que solo recorre los directorios del PATH buscando ficheros. cd no es un fichero: es código dentro del propio Bash. Un proceso externo no puede ver los builtins de su padre.

Solución al Ejercicio 2

  1. Por qué falla: el bucle while está al lado derecho de una tubería, así que Bash lo ejecuta en un subshell. Ese subshell recibe una copia de incidencias, la incrementa correctamente y muere al terminar el bucle. El shell principal nunca ve el cambio, y el echo final lee su propia variable, que sigue valiendo 0. Es exactamente el problema de la sección 5.2.

  2. Corrección A: sustitución de procesos (elimina la tubería, el bucle corre en el shell principal):

#!/usr/bin/env bash
incidencias=0
while IFS=',' read -r id fecha ciudad repartidor estado importe; do
    if [ "$estado" = "incidencia" ]; then
        incidencias=$((incidencias + 1))
    fi
done < /srv/veloz/datos/envios.csv
echo "Incidencias: $incidencias"

Nótese que aquí ni siquiera hace falta < <(...): como solo queríamos leer un fichero, basta con una redirección directa < fichero. El cat original era innecesario (es el llamado uso inútil de cat).

Corrección B: delegar el conteo en una herramienta:

#!/usr/bin/env bash
incidencias=$(grep -c ',incidencia,' /srv/veloz/datos/envios.csv)
echo "Incidencias: $incidencias"

O de forma más precisa, comprobando exactamente el quinto campo con awk (Módulo 6):

incidencias=$(awk -F',' '$5 == "incidencia"' /srv/veloz/datos/envios.csv | wc -l)
  1. Cuál preferir: la B, con awk. Motivos: es una sola línea, no tiene el problema del subshell, y es mucho más rápida en ficheros grandes. Un bucle while read en Bash procesa unas pocas miles de líneas por segundo; awk procesa cientos de miles. Con un envios.csv de 11 MB la diferencia es de segundos frente a milisegundos. La regla general de Bash: el shell es para orquestar, no para procesar datos línea a línea. Lo veremos formalizado en 08-02.

Solución al Ejercicio 3

# 1
CIUDAD="Bilbao"
bash -c 'echo "Sin export: [$CIUDAD]"'
export CIUDAD
bash -c 'echo "Con export: [$CIUDAD]"'
Sin export: []
Con export: [Bilbao]

Sin export, CIUDAD es una variable de shell y no forma parte del entorno que hereda el hijo. Con export, pasa a formar parte del entorno y viaja en la copia.

# 2
comando_inexistente_veloz
echo "Código A: $?"

ls --opcion-invalida 2> /dev/null
echo "Código B: $?"
bash: comando_inexistente_veloz: orden no encontrada
Código A: 127
Código B: 2

127 es el código convencional para "comando no encontrado" y 2 para "uso incorrecto".

# 3
grep 'ERROR' /fichero/que/no/existe | wc -l
echo "Código: $?"
grep: /fichero/que/no/existe: No existe el archivo o el directorio
0
Código: 0

$? vale 0 porque en una tubería refleja el estado del último comando. grep falló con código 2, pero wc -l recibió una entrada vacía, contó cero líneas y terminó correctamente. Para detectar el fallo real:

grep 'ERROR' /fichero/que/no/existe | wc -l
echo "Estados de la tubería: ${PIPESTATUS[@]}"
Estados de la tubería: 2 0

El array PIPESTATUS guarda el código de cada elemento. La alternativa habitual en scripts es set -o pipefail, que hace que la tubería devuelva el primer código distinto de cero (05-03).

# 4
if command -v jq > /dev/null 2>&1; then
    echo "jq está instalado en $(command -v jq)"
else
    echo "jq NO está instalado"
fi
jq NO está instalado

La redirección > /dev/null 2>&1 descarta tanto la salida normal como los errores: solo nos interesa el código de salida de command -v, no su texto. Las redirecciones se estudian en 02-04.

Conclusión

Ya tienes el modelo mental que sostiene todo lo que viene. Sabes que Bash trocea la línea, la expande, resuelve el comando en un orden estricto (alias → keyword → función → builtin → PATH) y, si es externo, hace fork y exec para ejecutarlo en un hijo. Entiendes por qué type -a es fiable y which no, cómo funciona la caché del PATH, y —lo más rentable de todo— por qué las variables no sobreviven a un subshell, lo que explica de una vez el comportamiento de las tuberías, los paréntesis, source y el propio cd. Y has tenido tu primer contacto con $?, la pieza sobre la que se construirá todo el control de errores del curso.

Con esto cierras el conocimiento fundamental del shell. Queda una habilidad transversal antes de pasar a los comandos: saber resolver dudas por tu cuenta. Ningún profesional recuerda todas las opciones de find ni el formato exacto de un crontab; lo que sabe es dónde mirarlo en diez segundos. En la siguiente lección, Encontrar Ayuda: man, help y --help, aprenderás a leer páginas de manual, a distinguir cuándo usar man y cuándo help, y a verificar comandos peligrosos antes de ejecutarlos en srv-veloz-01.

Curso de Programación en Bash

Módulo 1: Introducción a Bash

Módulo 2: Comandos Básicos de Bash

Módulo 3: Fundamentos de Scripting

Módulo 4: Scripting Intermedio

Módulo 5: Técnicas Avanzadas de Scripting

Módulo 6: Trabajando con Herramientas Externas

Módulo 7: Automatización y Programación

Módulo 8: Mejores Prácticas y Optimización

Módulo 9: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados