Shell scripts en Linux — teoría con un corrector de prácticas de C, parámetros que llegan partidos y un if que no compara nada (y el script que siguió como si nada)

En mi grupo de IC2 teníamos una costumbre: antes de entregar, cada uno dejaba su .c en una carpeta compartida y comprobábamos que todo compilaba. Al tercer día de hacerlo a mano me harté y pensé: «esto lo escribo en un script y listo». Abrí el editor, escribí cuatro líneas y lo lancé muy orgulloso:

#!/bin/bash
# corregir.sh: mi primera versión (cuenta las entregas)
total = 0
for f in entregas/*.c; do
    total=$((total + 1))
done
echo "Entregas: $total"

Esto es lo que me contestó la terminal:

$ ./corregir.sh
bash: ./corregir.sh: Permission denied
$ chmod +x corregir.sh
$ ./corregir.sh
./corregir.sh: line 3: total: command not found
Entregas: 5

Dos sustos seguidos. El primero, Permission denied: el archivo no tenía permiso de ejecución. El segundo es más traicionero: total: command not found en la línea 3 y, aun así, el script siguió como si nada y escribió Entregas: 5. Bash no se para cuando una línea falla: avisa y pasa a la siguiente. Ese día aprendí que un script puede «funcionar» estando mal, y que los espacios en bash no son decoración.

Hoy vamos a ver todo lo que necesitas para escribir scripts de verdad: la primera línea #!/bin/bash, chmod +x, variables, parámetros ($1, $#, "$@"), el código de salida $?, read, if con test, bucles, case y funciones. Todo con el mismo ejemplo: un corrector de prácticas de C que recorre una carpeta entregas/ con cinco entregas (ana.c, dani.c, laura.c, pablo.c y sergio.c) de la práctica «suma» (leer dos enteros y escribir su suma). Viene de Redirecciones y tuberías en Linux: aquí vas a ver >, 2>, < y | dentro de los scripts. Después te espera la práctica.

Qué es un shell script

Un shell script es un archivo de texto con órdenes de la terminal, una detrás de otra. Lo que escribirías a mano, pero guardado para repetirlo cuando quieras. Bash lo lee de arriba abajo y ejecuta cada línea como si la hubieras tecleado tú.

¿Y por qué no escribir un programa en C para eso? Porque la shell ya sabe hacer muy bien lo que más se repite en un sistema: lanzar programas (gcc, ls, grep), mirar si han ido bien y mover archivos. Lo que en C serían cien líneas de fork, exec y wait (lo viste en Procesos en C), en bash es una línea.

Todas las sesiones de este artículo se han ejecutado una sola vez en un Linux real, como un usuario normal llamado sergio, con LANG=C (por eso los mensajes de error salen en inglés). En tu ordenador saldrán tu usuario y tus carpetas, pero la lógica será la misma.

Tu primer script: #!/bin/bash y chmod +x

Empecemos por lo mínimo. Este es hola.sh:

#!/bin/bash
# hola.sh: el primer script del corrector
echo "Corrector de IC2 listo"
echo "Lo ejecuta: $USER"

Tres partes que vas a ver en todos los scripts:

  • #!/bin/bash (el shebang): siempre en la primera línea, sin espacios delante. Le dice al sistema con qué programa hay que leer el archivo.
  • Comentarios con #: bash ignora todo lo que va detrás. En la primera línea, #! es especial; en las demás, # es un comentario normal.
  • Órdenes: echo escribe un texto. $USER es una variable que ya existe y guarda tu nombre de usuario.

Ahora vamos a lanzarlo de cuatro formas distintas. stat -c '%A %n' enseña solo los permisos y el nombre (es como ls -l, pero sin tamaño ni fecha):

$ stat -c '%A %n' hola.sh
-rw-r--r-- hola.sh
$ ./hola.sh
bash: ./hola.sh: Permission denied
$ bash hola.sh
Corrector de IC2 listo
Lo ejecuta: sergio
$ chmod +x hola.sh
$ stat -c '%A %n' hola.sh
-rwxr-xr-x hola.sh
$ ./hola.sh
Corrector de IC2 listo
Lo ejecuta: sergio
$ hola.sh
bash: hola.sh: command not found
$ echo $PATH
/usr/local/bin:/usr/bin:/bin

Pasan cuatro cosas distintas, y las cuatro tienen explicación. Fíjate en el dibujo:

Cuatro formas de lanzar hola.sh Sin permiso de ejecución ./hola.sh da Permission denied; bash hola.sh funciona porque bash solo lee el archivo; tras chmod +x, ./hola.sh funciona gracias a la línea #!/bin/bash; hola.sh sin ./ se busca en PATH y da command not found. Cuatro formas de lanzar el mismo script ./hola.sh sin la x El sistema mira los permisos y no hay x: no lo ejecuta Permission denied bash hola.sh sin la x bash abre el archivo y lo lee como texto: le basta con la r Funciona ./hola.sh tras chmod +x Hay x: el sistema lee #!/bin/bash y le pasa el archivo a /bin/bash Funciona hola.sh sin ./ Sin ./ lo busca en las carpetas de $PATH: /usr/local/bin, /usr/bin, /bin command not found
Fíjate en la columna de la derecha: solo hay dos fallos, y ninguno es culpa del contenido del script.
  • ./hola.sh sin la x → Permission denied. El archivo se crea con -rw-r--r-- (lo viste en Permisos en Linux: la umask 022 nunca da x a un archivo nuevo).
  • bash hola.sh funciona aunque no haya x. Aquí no ejecutas el archivo: ejecutas bash y le pasas el archivo para que lo lea. Para leer basta con la r.
  • chmod +x hola.sh pone la x para dueño, grupo y otros (-rwxr-xr-x). A partir de ahí, ./hola.sh funciona: el sistema abre el archivo, ve #!/bin/bash en la primera línea y le pasa el archivo a /bin/bash.
  • hola.sh sin ./ → command not found. Cuando escribes un nombre sin barra, bash no lo busca en la carpeta actual: lo busca en las carpetas de $PATH (aquí /usr/local/bin, /usr/bin y /bin). El ./ significa «el de esta carpeta».

¿y por qué Linux no busca en la carpeta actual como hace Windows? Por seguridad. Si alguien deja en una carpeta compartida un script llamado ls, no quieres ejecutarlo sin querer cada vez que entras en ella.

El orden habitual al crear un script es siempre el mismo: escribirlo, chmod +x script.sh una sola vez y, desde entonces, ./script.sh. Si el script vive en tu carpeta, bash script.sh sirve para probarlo rápido sin tocar permisos.

Variables: sin espacios y con comillas

Una variable de bash guarda un texto. Se crea escribiendo nombre=valor y se usa con un $ delante. Mira esta sesión, que tiene tres trampas dentro:

$ alumno=laura
$ echo "Corrigiendo a $alumno"
Corrigiendo a laura
$ echo 'Corrigiendo a $alumno'
Corrigiendo a $alumno
$ alumno = laura
bash: alumno: command not found
$ echo "Archivo: $alumno.c"
Archivo: laura.c
$ echo "Archivo: $alumno_v2.c"
Archivo: .c
$ echo "Archivo: ${alumno}_v2.c"
Archivo: laura_v2.c
$ n=$(ls entregas | wc -l)
$ echo "Hay $n entregas"
Hay 5 entregas
$ archivo="practica final.c"
$ touch "$archivo"
$ rm $archivo
rm: cannot remove 'practica': No such file or directory
rm: cannot remove 'final.c': No such file or directory
$ rm "$archivo"

Vamos línea a línea, porque aquí está la mitad de los errores de los scripts:

OrdenQué pasaPor qué
alumno=lauraCrea la variableSin espacios alrededor del =
echo "Corrigiendo a $alumno"Corrigiendo a lauraDentro de comillas dobles, $alumno se sustituye
echo 'Corrigiendo a $alumno'Corrigiendo a $alumnoDentro de comillas simples, nada se sustituye: el texto va tal cual
alumno = lauraalumno: command not foundCon espacios, bash entiende «ejecuta la orden alumno con los parámetros = y laura»
echo "Archivo: $alumno_v2.c"Archivo: .cBash lee el nombre más largo posible: busca la variable alumno_v2, que no existe, y la cambia por nada
echo "Archivo: ${alumno}_v2.c"Archivo: laura_v2.cLas llaves marcan dónde acaba el nombre
n=$(ls entregas | wc -l)n vale 5$( ) ejecuta la orden y pone su salida en su lugar

La última parte de la sesión es la más importante. archivo="practica final.c" guarda un nombre con un espacio. touch "$archivo" lo crea bien. Pero rm $archivo, sin comillas, intenta borrar dos archivos: practica y final.c, que no existen. Bash sustituye la variable y, después, parte el resultado por los espacios. Con rm "$archivo" llega un solo parámetro y se borra el archivo correcto (por eso esa orden no escribe nada: en Linux, el silencio significa que todo ha ido bien).

Escribir $variable sin comillas. Funciona mientras los datos no tengan espacios… y falla el día que alguien entrega practica final.c. La regla práctica: pon siempre las variables entre comillas dobles, "$variable", salvo que sepas exactamente por qué no.

No hay tipos. Para bash, nota=7 guarda el texto 7. Solo se convierte en número cuando haces cuentas con $(( )) o comparas con -eq, -lt… Lo vemos en la sección de if.

Parámetros: $0, $1… $# y «$@»

Cuando lanzas ./script.sh a b c, lo que va detrás del nombre son los parámetros (o argumentos), y el script los recibe en variables especiales. Este script enseña todas:

#!/bin/bash
# parametros.sh: enseña lo que recibe el script
echo "Me llamo: $0"
echo "Primer parámetro: $1"
echo "Segundo parámetro: $2"
echo "Número de parámetros: $#"
echo "Recorriendo \"\$@\":"
for p in "$@"; do
    echo "  [$p]"
done
echo "Recorriendo \$*:"
for p in $*; do
    echo "  [$p]"
done

Lo llamamos con un parámetro normal y otro con un espacio dentro, y después sin nada:

$ chmod +x parametros.sh
$ ./parametros.sh ana.c "practica final.c"
Me llamo: ./parametros.sh
Primer parámetro: ana.c
Segundo parámetro: practica final.c
Número de parámetros: 2
Recorriendo "$@":
  [ana.c]
  [practica final.c]
Recorriendo $*:
  [ana.c]
  [practica]
  [final.c]
$ ./parametros.sh
Me llamo: ./parametros.sh
Primer parámetro: 
Segundo parámetro: 
Número de parámetros: 0
Recorriendo "$@":
Recorriendo $*:
  • $0 es el nombre con el que se ha llamado al script (./parametros.sh). Muy útil en los mensajes de uso.
  • $1, $2… son los parámetros por orden. A partir del décimo hay que escribir ${10}.
  • $# es cuántos parámetros hay: 2, aunque el segundo tenga un espacio, porque las comillas al llamar lo convierten en uno solo.
  • "$@" son todos los parámetros, cada uno entero. El bucle da dos vueltas.
  • $* sin comillas junta todos y los vuelve a partir por los espacios. El bucle da tres vueltas y rompe practica final.c en dos.

En la segunda llamada no hay parámetros: $1 y $2 valen cadena vacía (no es un error, simplemente no hay nada) y $# es 0. Así es como un script sabe que lo han llamado mal.

Parámetros posicionales y la diferencia entre «$@» y $* La orden ./parametros.sh ana.c «practica final.c» reparte $0, $1 y $2, con $# igual a 2. «$@» da dos vueltas con cada parámetro entero; $* da tres porque parte «practica final.c» por el espacio. Lo que recibe el script $ ./parametros.sh $0 ana.c $1 «practica final.c» $2 $# = 2 for p in «$@» cada parámetro entero, tal cual llegó [ana.c] [practica final.c] for p in $* todo junto y vuelto a partir por espacios [ana.c] [practica] [final.c] 3 vueltas, no 2
Las comillas de la orden protegen el espacio al llamar; dentro del script, solo «$@» lo sigue protegiendo.

Para recorrer los parámetros, siempre for p in "$@". Es la única forma que respeta los nombres con espacios. $* sin comillas solo tiene sentido si quieres partirlos a propósito.

El código de salida: $? y exit

Esta es la idea que más cambia cómo escribes scripts. Toda orden de Linux termina devolviendo un número, su código de salida (exit status):

  • 0 significa que todo ha ido bien.
  • Cualquier otro número (1, 2, 127…) significa que algo ha fallado. Cada programa elige qué número usa para cada fallo.

La variable $? guarda el código de la última orden. Mira lo que devuelven gcc y ls:

$ gcc entregas/ana.c -o ana
$ echo $?
0
$ gcc entregas/pablo.c -o pablo 2> /dev/null
$ echo $?
1
$ ls pablo
ls: cannot access 'pablo': No such file or directory
$ echo $?
2
$ gcc entregas/laura.c -o laura && echo "laura compila"
laura compila
$ gcc entregas/pablo.c -o pablo 2> /dev/null || echo "pablo no compila"
pablo no compila
$ chmod +x comprobar.sh
$ ./comprobar.sh
Uso: ./comprobar.sh archivo.c
$ echo $?
2
$ ./comprobar.sh entregas/nadie.c
No existe: entregas/nadie.c
$ echo $?
1
$ ./comprobar.sh entregas/ana.c
OK: entregas/ana.c existe
$ echo $?
0

Repasemos las cuentas de la primera mitad: gcc con ana.c compila → 0. Con pablo.c (le falta un ;) falla → 1. El 2> /dev/null tira los mensajes de error, porque aquí solo nos interesa el código. ls pablo falla porque el ejecutable no se ha creado, y ls usa el 2 para «no existe». Cada programa tiene sus números; lo único universal es que 0 = bien.

Después aparecen && y ||, que deciden con ese código:

  • orden1 && orden2: ejecuta orden2 solo si orden1 ha devuelto 0 (laura compila).
  • orden1 || orden2: ejecuta orden2 solo si orden1 ha fallado (pablo no compila).

Tus scripts también devuelven un código, y lo eliges con exit. Este es comprobar.sh, que usa tres:

#!/bin/bash
# comprobar.sh: ¿me han pasado exactamente un archivo que exista?
if [ $# -ne 1 ]; then                 # -ne: "no igual", comparación de NÚMEROS
    echo "Uso: $0 archivo.c" >&2       # >&2: los errores van por stderr
    exit 2                            # 2 = me has llamado mal
fi
if [ ! -f "$1" ]; then                # ! niega: "si NO existe como archivo"
    echo "No existe: $1" >&2
    exit 1                            # 1 = algo ha ido mal
fi
echo "OK: $1 existe"
exit 0                                # 0 = todo bien

Las tres últimas llamadas de la sesión de arriba lo prueban: sin parámetros → mensaje de uso y 2; con un archivo que no existe → 1; con entregas/ana.c → OK y 0. El que llame al script (una persona, otro script o un if) puede saber qué ha pasado sin leer los mensajes.

Cómo decide if con el código de salida if ejecuta la orden y, si su código de salida $? es 0, entra en then; si no, en else. A la derecha, los códigos reales: gcc da 0 al compilar y 1 al fallar, ls da 2 si no existe el archivo y comprobar.sh devuelve 2, 1 o 0. if no compara: ejecuta una orden y mira su $? if gcc ana.c -o ana; then 1. ejecuta la orden 2. ¿$? es 0? sí then no else Códigos reales de la ejecución orden $? gcc entregas/ana.c -o ana compila 0 gcc entregas/pablo.c -o pablo error de compilación 1 ls pablo no existe 2 ./comprobar.sh me has llamado mal 2 ./comprobar.sh entregas/nadie.c algo ha ido mal 1 ./comprobar.sh entregas/ana.c todo bien 0
En la shell, 0 significa «bien»: justo al revés que en un if de C, donde 0 es falso.

Pensar como en C, donde 0 es falso. En la shell, 0 es «bien», y if entra en el then cuando la orden devuelve 0. Si un script tuyo termina con exit 1 cuando todo ha ido bien, cualquier if ./tu_script.sh creerá que ha fallado.

El >&2 de comprobar.sh manda el mensaje por stderr, como viste en Redirecciones. Así, quien haga ./comprobar.sh > informe.txt no mete los errores en el informe. Los mensajes de error, por stderr; los resultados, por stdout.

Leer datos con read

read espera una línea por la entrada estándar y la guarda en una variable. Este script pregunta por un alumno:

#!/bin/bash
# preguntar.sh: pide un nombre y dice si ha entregado
echo "¿Qué alumno corrijo?"
read -r alumno                        # espera una línea y la guarda en alumno
if [ -f "entregas/$alumno.c" ]; then
    echo "$alumno ha entregado"
else
    echo "$alumno no ha entregado"
fi

Si lo lanzas en tu terminal, se queda esperando a que escribas un nombre y pulses Intro. Aquí le damos la respuesta de dos formas que ya conoces: con <<< (una cadena como entrada) y con una tubería:

$ chmod +x preguntar.sh
$ ./preguntar.sh <<< "laura"
¿Qué alumno corrijo?
laura ha entregado
$ echo "marta" | ./preguntar.sh
¿Qué alumno corrijo?
marta no ha entregado
  • read -r alumno lee una línea entera y la guarda en alumno. La -r hace que las barras \ se lean tal cual (sin ella, bash las trata como escapes). Úsala siempre.
  • Si pones varias variables, read -r alumno nota, la primera palabra va a alumno y todo lo demás a nota. Lo usarás en los ejercicios.
  • read devuelve 0 si ha podido leer y 1 si se ha acabado la entrada. Por eso while read sirve para recorrer un archivo línea a línea (lo vemos en los bucles).

Para un corrector automático, read es más útil para leer archivos que para preguntar al usuario. Un script que pregunta no se puede usar dentro de otro script ni dejar trabajando solo; uno que recibe parámetros, sí.

Condiciones con if y test

Aquí está la sorpresa del título: if no compara nada. if ejecuta una orden y mira su código de salida. Si es 0, entra en el then; si no, en el else:

if orden; then
    # orden ha devuelto 0
else
    # orden ha devuelto otra cosa
fi

Por eso puedes escribir if gcc entregas/ana.c -o ana; then. ¿Y para comparar? Para eso existe una orden llamada test, que también se escribe [ (sí, [ es el nombre de un programa, y ] es su último parámetro obligatorio). test no escribe nada: solo devuelve 0 si la condición se cumple y 1 si no. Mírala por separado:

$ [ -f entregas/ana.c ]
$ echo $?
0
$ [ -d entregas/ana.c ]
$ echo $?
1
$ [ 10 -gt 9 ]
$ echo $?
0
$ [[ 10 > 9 ]]
$ echo $?
1
$ [[ 10 < 9 ]]
$ echo $?
0
$ [ "laura" = "laura" ]
$ echo $?
0
$ suma=5+2
$ echo $suma
5+2
$ echo $((5 + 2))
7
$ echo $((7 / 2))
3
$ nota=7
$ nota=$((nota + 2))
$ echo $nota
9

Lo más importante de esta sesión son dos resultados que parecen al revés:

  • [ 10 -gt 9 ] → 0 (verdadero): -gt compara números, y 10 es mayor que 9.
  • [[ 10 > 9 ]] → 1 (falso) y [[ 10 < 9 ]] → 0 (verdadero). Con > y < se comparan cadenas, letra a letra, como en un diccionario: el 1 va antes que el 9, así que "10" va antes que "9".

Y las cuentas: suma=5+2 guarda el texto 5+2, sin sumar. Para hacer cuentas se usa $(( )): $((5 + 2)) da 7 y $((7 / 2)) da 3, porque bash solo trabaja con enteros (7 entre 2 es 3 y sobra 1). Por último, nota=$((nota + 2)) con nota=7 deja 7 + 2 = 9.

Estos son los operadores de test que vas a usar el 95 % de las veces:

Quiero saber…OperadorEjemplo
Si existe y es un archivo-f[ -f entregas/ana.c ]
Si existe y es una carpeta-d[ -d entregas ]
Si existe y se puede ejecutar-x[ -x entregas/ana ]
Si existe y no está vacío-s[ -s errores/sergio.txt ]
Números: igual / distinto-eq / -ne[ $# -ne 1 ]
Números: menor / mayor-lt / -gt[ "$nota" -gt 3 ]
Números: menor o igual / mayor o igual-le / -ge[ "$nota" -ge 5 ]
Cadenas: igual / distinta= / !=[ "$modo" = compilar ]
Cadena vacía / no vacía-z / -n[ -z "$1" ]
Negar![ ! -f "$f" ]

Ahora, todo junto en un script con if, elif y else. clasificar.sh pasa una nota a palabra:

#!/bin/bash
# clasificar.sh: pasa una nota (0-10) a palabra
nota=$1
if [ -z "$nota" ]; then               # -z: la cadena está vacía (no me han dado nota)
    echo "Falta la nota" >&2
    exit 2
elif [ "$nota" -ge 9 ]; then          # -ge: mayor o igual, comparando NÚMEROS
    echo "Sobresaliente"
elif [ "$nota" -ge 7 ]; then
    echo "Notable"
elif [ "$nota" -ge 5 ]; then
    echo "Aprobado"
else
    echo "Suspenso"
fi

Y así responde con distintas notas:

$ chmod +x clasificar.sh
$ ./clasificar.sh 9
Sobresaliente
$ ./clasificar.sh 6
Aprobado
$ ./clasificar.sh
Falta la nota
$ ./clasificar.sh siete
./clasificar.sh: line 7: [: siete: integer expression expected
./clasificar.sh: line 9: [: siete: integer expression expected
./clasificar.sh: line 11: [: siete: integer expression expected
Suspenso

Con 9 entra en la primera rama (9 ≥ 9). Con 6 falla las dos primeras (6 < 9 y 6 < 7) y entra en la de -ge 5. Sin parámetro, -z detecta la cadena vacía. Pero mira lo que pasa con siete: -ge necesita números, así que [ se queja en las líneas 7, 9 y 11 (integer expression expected), cada condición falla y el script acaba en el else, con un Suspenso que no es verdad. Otra vez el mismo aviso que en el gancho: bash no se para cuando algo falla.

Escribir [$# -ne 1] o [ $#-ne 1 ]. [ es una orden y cada pieza es un parámetro, así que necesitan espacios: [ $# -ne 1 ]. Sin el espacio, bash busca una orden llamada [1 y no la encuentra. Lo verás en directo en los ejercicios.

[[ ]] es la versión moderna de [ ] que trae bash (no es un programa aparte). Admite && y || dentro y no parte las variables por los espacios, pero no existe en todas las shells. En IC2, con [ ] y las variables entre comillas vas sobrado.

Bucles: for y while

for recorre una lista de palabras. La lista más útil en un corrector es entregas/*.c, que bash cambia por los nombres de los archivos que encajan, en orden alfabético:

#!/bin/bash
# contar.sh: recorre las entregas y numera cada una
n=0
for f in entregas/*.c; do             # el * lo expande bash, en orden alfabético
    n=$((n + 1))
    echo "$n: $(basename "$f")"
done
echo "Total: $n entregas"

while repite mientras una orden devuelva 0. Con read sirve para recorrer un archivo línea a línea. Este script mira quién de la lista alumnos.txt (ana, dani, laura, marta, pablo, ruben y sergio) no ha entregado:

#!/bin/bash
# pendientes.sh: ¿quién de la lista no ha entregado?
faltan=0
while read -r alumno; do              # una vuelta por cada línea de alumnos.txt
    if [ ! -f "entregas/$alumno.c" ]; then
        echo "Falta: $alumno"
        faltan=$((faltan + 1))
    fi
done < alumnos.txt                    # el < va en el done: alimenta a todo el while
echo "Sin entregar: $faltan"

Los dos en acción:

$ chmod +x contar.sh pendientes.sh
$ ./contar.sh
1: ana.c
2: dani.c
3: laura.c
4: pablo.c
5: sergio.c
Total: 5 entregas
$ ./pendientes.sh
Falta: marta
Falta: ruben
Sin entregar: 2

En contar.sh, n empieza en 0 y suma 1 en cada vuelta. basename "$f" quita la carpeta del nombre (entregas/ana.c → ana.c). Así va el contador:

Vueltafn despuésEscribe
1entregas/ana.c11: ana.c
2entregas/dani.c22: dani.c
3entregas/laura.c33: laura.c
4entregas/pablo.c44: pablo.c
5entregas/sergio.c55: sergio.c

En pendientes.sh, el < alumnos.txt va detrás del done: así el archivo entero alimenta al bucle y cada read toma la línea siguiente. Cuando se acaban las líneas, read devuelve 1 y el while termina. De siete alumnos, faltan marta y ruben: 7 − 5 = 2, que es justo Sin entregar: 2.

Recorrer archivos con for f in $(ls entregas). Con nombres con espacios se rompe (el mismo problema que rm $archivo) y además pierdes la ruta. Usa el comodín directamente: for f in entregas/*.c.

case: elegir según un patrón

Cuando una variable puede valer muchas cosas, una cadena de elif se vuelve ilegible. case compara un valor con patrones (los mismos comodines que en la terminal: *, ?, [0-9]):

#!/bin/bash
# opcion.sh: decide qué hacer según la opción
case "$1" in
    -c|--compilar)                    # | separa patrones que hacen lo mismo
        echo "Solo compilo" ;;
    -p|--probar)
        echo "Compilo y pruebo" ;;
    -h|--ayuda|"")                    # "" = no me han pasado nada
        echo "Uso: $0 -c | -p | -h" ;;
    *)                                # * = cualquier otra cosa (va la última)
        echo "Opción desconocida: $1" >&2
        exit 2 ;;
esac

Lo probamos con una opción corta, una larga, sin nada y con una que no existe:

$ chmod +x opcion.sh
$ ./opcion.sh -c
Solo compilo
$ ./opcion.sh --probar
Compilo y pruebo
$ ./opcion.sh
Uso: ./opcion.sh -c | -p | -h
$ ./opcion.sh -x
Opción desconocida: -x
$ echo $?
2
  • Cada rama termina en ;;. El primer patrón que encaja gana y no se miran los demás.
  • | separa varios patrones para la misma rama: -c|--compilar.
  • "" encaja con la cadena vacía (no han pasado nada), así que sin parámetros sale la ayuda.
  • * encaja con todo, por eso va la última: es el «en cualquier otro caso». Con -x escribe el error y termina con exit 2, que vemos con echo $?.

case es perfecto para dos cosas en los scripts: las opciones (-c, -h…) y los tipos de archivo (*.c), *.h), *.txt)). Lo usarás en la práctica para las opciones del corrector.

Funciones

Una función es un trozo de script con nombre, que llamas como si fuera una orden. Recibe sus propios parámetros ($1, $2 dentro de la función son los de la llamada, no los del script) y devuelve un código con return:

#!/bin/bash
# funciones.sh: compilar() hace el trabajo y devuelve un código
compilar() {
    local fuente=$1                   # local: solo existe dentro de la función
    local binario=${fuente%.c}        # quita el .c del final: entregas/ana
    gcc -Wall "$fuente" -o "$binario" 2> /dev/null
    return $?                         # el código de gcc pasa a ser el de la función
}

echo "El script recibió: $1"
for f in entregas/ana.c entregas/pablo.c; do
    if compilar "$f"; then            # if mira el código que devuelve compilar
        echo "OK     $f"
    else
        echo "ERROR  $f"
    fi
done
echo "fuente fuera de la función: '$fuente'"

Lo lanzamos pasándole hola como parámetro, para ver que la función no lo confunde con el suyo:

$ chmod +x funciones.sh
$ ./funciones.sh hola
El script recibió: hola
OK     entregas/ana.c
ERROR  entregas/pablo.c
fuente fuera de la función: ''

Fíjate en cuatro detalles:

  • Se define antes de usarla. Bash lee el script de arriba abajo; si llamas a compilar antes de definirla, command not found.
  • $1 dentro de la función es el archivo que le pasas (entregas/ana.c), aunque el script haya recibido hola como $1.
  • local hace que fuente y binario solo existan dentro de la función. Por eso la última línea escribe '': fuera, fuente no existe. Sin local, la variable se quedaría «pegada» y podría pisar otra del mismo nombre.
  • return devuelve un código (0-255), no un texto. if compilar "$f" mira ese código igual que miraría el de gcc. Para que una función «devuelva» un texto, la función lo escribe con echo y quien llama lo recoge con $(compilar …).

${fuente%.c} es una expansión que quita .c del final: entregas/ana.c → entregas/ana. Es la forma rápida de sacar el nombre del ejecutable.

Confundir return con exit dentro de una función. return sale de la función y el script sigue; exit termina el script entero. En la práctica lo usaremos a propósito en una función uso que tiene que cortar todo.

Errores típicos

Espacios alrededor del = (total = 0). Bash lo toma como la orden total y da command not found. Las asignaciones van pegadas: total=0.

Olvidar los espacios dentro de los corchetes ([$# -ne 1]). [ es una orden: necesita un espacio detrás y otro antes de ].

Comparar números con > o < dentro de [ ]. Ahí > es una redirección: crea un archivo y la condición sale verdadera. Para números, -gt, -lt, -ge…

Mirar $? tarde. $? es el código de la última orden: si entre gcc y tu if [ $? -eq 0 ] hay un echo, estás mirando el código del echo (que siempre es 0). Guárdalo en una variable justo después o usa if gcc … directamente.

El script no arranca. Permission denied → falta chmod +x. command not found → falta el ./. bad interpreter → el #! no está en la primera línea o tiene una ruta mal escrita.

Pruébalo en tu terminal

Este tema no tiene Python Tutor (no es Python, ni C), pero tu terminal es todavía mejor: puedes escribir un script en diez segundos con cat y un here-document y probarlo. Abre una terminal en una carpeta de pruebas y teclea esto (la salida es la de nuestra ejecución):

$ cat > saludo.sh << 'EOF'
> #!/bin/bash
> echo "Hola, $1"
> echo "Me has pasado $# parámetros"
> EOF
$ ./saludo.sh Laura
bash: ./saludo.sh: Permission denied
$ chmod +x saludo.sh
$ ./saludo.sh Laura
Hola, Laura
Me has pasado 1 parámetros
$ ./saludo.sh Ana Ruiz
Hola, Ana
Me has pasado 2 parámetros
$ ./saludo.sh "Ana Ruiz"
Hola, Ana Ruiz
Me has pasado 1 parámetros
$ echo $?
0

Qué observar:

  1. El here-document (<< 'EOF') crea el archivo sin abrir ningún editor. Las comillas de 'EOF' hacen que $1 y $# se guarden tal cual en el archivo, sin sustituirse al crearlo.
  2. El primer ./saludo.sh Laura da Permission denied: un archivo recién creado nunca tiene x. Después de chmod +x, funciona.
  3. ./saludo.sh Ana Ruiz son dos parámetros: $1 es solo Ana y $# vale 2. Con comillas, "Ana Ruiz" es uno.
  4. echo $? da 0: el script no tiene exit, así que devuelve el código de su última orden, el echo, que ha ido bien.
  5. Cambia el script para que termine con exit 3 y vuelve a mirar echo $?. Así compruebas que el código lo decides tú.

Resumen

ConceptoCómo se escribeLo que no hay que olvidar
Primera línea#!/bin/bashEn la línea 1, sin espacios delante
Ejecutarchmod +x s.sh y ./s.shSin ./ se busca en $PATH
Variablex=5, "$x", ${x}_v2Sin espacios en el =; usar entre comillas dobles
Salida de una ordenn=$(ls | wc -l)$( ) ejecuta y sustituye
Parámetros$0, $1, $#, "$@""$@" respeta los espacios; $* no
Código de salida$?, exit 2, return 10 = bien; mirar $? justo después
Leerread -r xDevuelve 1 cuando se acaba la entrada
Condiciónif orden; then … fi, [ "$n" -gt 3 ]if mira un código; [ ] necesita espacios
Cuentas$((n + 1))Solo enteros: 7 / 2 = 3
Buclesfor f in dir/*.c, while read -r l; do … done < fEl < va en el done
Elegircase "$1" in -c) … ;; *) … ;; esac* va la última
Funcionesf() { local x=$1; …; return 0; }Definir antes de usar; return ≠ exit

Tres ideas para quedarte

  1. En bash, los espacios significan algo. Separan una orden de sus parámetros: por eso x = 5 falla, [ ] los necesita y las variables van entre comillas.
  2. Todo gira alrededor del código de salida. if, while, && y || no miran textos: miran si la orden ha devuelto 0. Y tus scripts deben devolver códigos que signifiquen algo.
  3. Bash no se para cuando algo falla. Avisa y sigue. Por eso un buen script comprueba sus parámetros al principio y termina con exit en cuanto algo no cuadra.

Con esto ya puedes leer y escribir scripts de verdad. En la práctica de shell scripts construimos el corrector completo: compilar todas las entregas, probar sus salidas y poner la nota, con opciones y códigos de salida. Y después, en la recta final del Linux de IC2, tocará vi y la configuración de red.

Siguiente paso: prácticaShell scripts en Linux — práctica con un corrector que compila, prueba y pone nota a las entregas de C (y la práctica que «compilaba» sin compilar)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *