Shell scripts en Linux — ejercicios con parámetros que llevan espacios, notas que hay que validar y una lista de alumnos que corregir (y el archivo «3» que apareció en la carpeta)
Un lunes abrí la carpeta compartida del grupo de IC2 y había un archivo nuevo, vacío, que se llamaba 3. Nadie lo había creado. O eso creíamos. Lo borré, volví a lanzar mi script de resumen y… ahí estaba otra vez. El culpable era una línea que parecía inofensiva: if [ $correctas > 3 ]; then. Dentro de los corchetes, el > no compara: redirige. Bash creaba el archivo 3 y, de paso, la condición salía siempre verdadera.
Así son los scripts: lo que parece una comparación puede ser una redirección, lo que parece un parámetro pueden ser dos, y un $? mirado una línea tarde dice lo contrario de lo que pasó. En estos ejercicios vas a escribir tres scripts del corrector de prácticas y a cazar tres fallos de los que se cuelan en los scripts de verdad.
Tabla de Contenidos
Cómo usar este artículo
- Lee el enunciado y la salida esperada de cada nivel. Esa salida es real: se ha obtenido ejecutando la solución una vez, como el usuario
sergio, conLANG=C. - Escribe tu script en una carpeta de pruebas con la misma estructura (
entregas/conana.c,dani.c,laura.c,pablo.cysergio.c, la práctica «suma» de la práctica). Usa la pista solo si te atascas. - Haz los tres fragmentos de «¿Qué falla aquí?» prediciendo antes de mirar la explicación.
- Compara con las soluciones y quédate con el chuletario del final.
Nivel 1 (básico): información de varios archivos .c
Escribe info.sh, que recibe uno o más archivos .c como parámetros y, para cada uno, escribe cuántas líneas tiene y si contiene int main. Si un archivo no existe, lo dice y sigue con el siguiente. Sin parámetros, mensaje de uso por stderr y código 2. El script termina con código 0 si todos existen y tienen main, y 1 si no. Tiene que funcionar con nombres con espacios, como practica final.c (aquí, un archivo de una sola línea con un #include).
Salida esperada:
$ printf '#include <stdio.h>\n' > "practica final.c" $ chmod +x info.sh $ ./info.sh Uso: ./info.sh archivo.c [archivo.c ...] $ echo $? 2 $ ./info.sh entregas/ana.c "practica final.c" entregas/nadie.c entregas/ana.c: 8 líneas, tiene main practica final.c: 1 líneas, SIN main entregas/nadie.c: no existe $ echo $? 1 $ ./info.sh entregas/*.c entregas/ana.c: 8 líneas, tiene main entregas/dani.c: 8 líneas, tiene main entregas/laura.c: 8 líneas, tiene main entregas/pablo.c: 8 líneas, tiene main entregas/sergio.c: 9 líneas, tiene main $ echo $? 0
Recorre los parámetros con for f in "$@". Para contar líneas sin que salga el nombre, wc -l < "$f". Para saber si un archivo contiene un texto sin que se escriba nada, grep -q, y su código de salida en un if.
Nivel 2 (intermedio): calificaciones y media
Escribe notas.sh, que recibe un archivo con líneas alumno nota. Para cada línea, si la nota es un número del 0 al 10, escribe la calificación (Sobresaliente ≥ 9, Notable ≥ 7, Aprobado ≥ 5, Suspenso); si no, avisa de que no es válida y la salta. Al final, el número de notas válidas, de aprobados y de no válidas, y la media con un decimal. Código 2 si falta el parámetro, 1 si el archivo no existe o hay notas no válidas, y 0 si todo es correcto. El archivo notas.txt es:
ana 10 dani 5 laura 10 marta NP pablo 0 sergio 10
Salida esperada:
$ chmod +x notas.sh $ ./notas.sh notas.txt ana: 10 Sobresaliente dani: 5 Aprobado laura: 10 Sobresaliente marta: nota no válida (NP) pablo: 0 Suspenso sergio: 10 Sobresaliente Notas válidas: 5. Aprobados: 4. No válidas: 1 Media: 7.0 $ echo $? 1 $ ./notas.sh notas.csv No existe: notas.csv $ echo $? 1
while read -r alumno nota; do … done < "$1" reparte cada línea en dos variables. Para saber si es una nota válida, un case con los patrones [0-9]|10). Y como bash solo hace cuentas con enteros, el decimal sale de suma * 10 / n y del resto % 10.
Nivel 3 (desafío): corregir a toda la lista
Escribe revisar_lista.sh lista carpeta: recorre alumnos.txt (ana, dani, laura, marta, pablo, ruben y sergio) y, para cada alumno, escribe su nota: NO ENTREGADO si no hay .c, 0 si no compila, 5 si compila pero su salida con entrada.txt no es esperado.txt, y 10 si es correcta. Nombre alineado en 8 columnas. Al final, cuántos hay en la lista, cuántos han entregado y cuántos están bien. Código 2 si los parámetros no son dos, 1 si alguien no ha entregado, 0 si todos han entregado.
Salida esperada:
$ chmod +x revisar_lista.sh $ ./revisar_lista.sh alumnos.txt Uso: ./revisar_lista.sh lista carpeta $ echo $? 2 $ ./revisar_lista.sh alumnos.txt entregas ana 10 dani 5 laura 10 marta NO ENTREGADO pablo 0 ruben NO ENTREGADO sergio 10 En la lista: 7. Entregadas: 5. Correctas: 3 $ echo $? 1
Haz una función nota_de que escriba la nota con echo y devuelva 1 solo si el alumno no ha entregado. Desde el bucle, nota=$(nota_de "$alumno") recoge el texto y, justo después, $? tiene su código. Para alinear, printf "%-8s %s\n".
¿Qué falla aquí?
Tres scripts cortos con un fallo cada uno. Antes de leer la explicación, predice qué escribe cada uno.
Fragmento 1: el aviso de uso que nunca avisa
#!/bin/bash
# uno.sh: avisa si no me pasan exactamente una carpeta
if [$# -ne 1]; then
echo "Uso: $0 carpeta"
exit 2
fi
echo "Corrijo la carpeta $1"
Lo lanzamos con una carpeta y sin nada. ¿Qué crees que pasa en cada caso? Esta es la salida real:
$ chmod +x uno.sh $ ./uno.sh entregas ./uno.sh: line 3: [1: command not found Corrijo la carpeta entregas $ ./uno.sh ./uno.sh: line 3: [0: command not found Corrijo la carpeta
Bash ha leído [$# como una sola palabra. Tras sustituir $#, la orden que intenta ejecutar se llama [1 (o [0), que no existe: command not found. Ese error devuelve 127, que no es 0, así que el if va al else… que no existe, y el script sigue. Por eso, sin parámetros, escribe Corrijo la carpeta con un hueco: no ha avisado de nada.
Quitar los espacios de los corchetes. [ es una orden y ] su último parámetro: necesitan espacios, como cualquier orden y sus parámetros.
El arreglo es solo eso, dos espacios:
#!/bin/bash
# uno.sh: avisa si no me pasan exactamente una carpeta
if [ $# -ne 1 ]; then # espacio después de [ y antes de ]
echo "Uso: $0 carpeta"
exit 2
fi
echo "Corrijo la carpeta $1"
Y ahora sí:
$ chmod +x uno.sh $ ./uno.sh entregas Corrijo la carpeta entregas $ ./uno.sh Uso: ./uno.sh carpeta
Cuando veas command not found con un nombre raro que empieza por [, ya sabes qué buscar: un corchete pegado a lo que tiene al lado.
Fragmento 2: más de 3 con solo 2
#!/bin/bash
# comparar.sh: ¿han salido bien más de 3 prácticas?
correctas=2
if [ $correctas > 3 ]; then
echo "Más de 3 correctas"
else
echo "3 o menos"
fi
Con correctas=2, ¿debería escribir «3 o menos»? Esta es la salida real, y lo que hay en la carpeta después:
$ chmod +x comparar.sh $ ./comparar.sh Más de 3 correctas $ ls 3 comparar.sh $ cat 3
Escribe Más de 3 correctas y aparece un archivo llamado 3, vacío (por eso cat 3 no escribe nada). Es la anécdota del principio. Dentro de [ ], el > no es «mayor que»: es la redirección de siempre. Bash sustituye la variable, aparta el > 3, crea el archivo 3 para la salida de [ y ejecuta [ 2 ]. Con un solo parámetro, test comprueba si esa cadena no está vacía, y 2 no lo está: verdadero, código 0, then.
Comparar números con > o < dentro de [ ]. Para números, -gt, -lt, -ge, -le, -eq y -ne. Y dentro de [[ ]], > no redirige, pero compara cadenas: [[ 10 > 9 ]] es falso.
El arreglo usa -gt y, de paso, las comillas:
#!/bin/bash
# comparar.sh: ¿han salido bien más de 3 prácticas?
correctas=2
if [ "$correctas" -gt 3 ]; then # -gt compara números; > dentro de [ ] redirige
echo "Más de 3 correctas"
else
echo "3 o menos"
fi
La salida ya es la correcta, y en la carpeta no aparece ningún 3:
$ chmod +x comparar.sh $ ./comparar.sh 3 o menos $ ls comparar.sh
Fragmento 3: pablo compila (pero no)
#!/bin/bash
# compila.sh: dice si una entrega compila
gcc -Wall "$1" -o prog 2> /dev/null
echo "Compilación terminada"
if [ $? -eq 0 ]; then
echo "Compila"
else
echo "No compila"
fi
Le pasamos entregas/pablo.c, que no compila. Salida real:
$ chmod +x compila.sh $ ./compila.sh entregas/pablo.c Compilación terminada Compila
Compila. ¿Cómo puede ser? Porque $? es el código de la última orden, y entre gcc y el if hay un echo. El if está mirando el código del echo, que siempre ha ido bien (0). El 1 de gcc se perdió en cuanto se ejecutó la orden siguiente.
Mirar $? una línea tarde. Cualquier orden, incluso un echo o un [ ], cambia $?.
Dos arreglos posibles: if gcc …; then directamente, o guardar el código en una variable justo después. Aquí usamos el segundo, porque queremos escribir el mensaje antes de decidir:
#!/bin/bash
# compila.sh: dice si una entrega compila
gcc -Wall "$1" -o prog 2> /dev/null
rc=$? # guardamos el código JUSTO después de gcc
echo "Compilación terminada"
if [ "$rc" -eq 0 ]; then
echo "Compila"
else
echo "No compila"
fi
Con pablo y con ana:
$ chmod +x compila.sh $ ./compila.sh entregas/pablo.c Compilación terminada No compila $ ./compila.sh entregas/ana.c Compilación terminada Compila
Si vas a usar el código más de una vez, o hay algo entre la orden y la decisión, guárdalo siempre: rc=$? en la línea siguiente, y después trabaja con $rc.
Soluciones
Solución del nivel 1
#!/bin/bash
# info.sh: para cada archivo .c que recibe, cuenta sus líneas y mira si tiene main
# Uso: ./info.sh archivo.c [archivo.c ...]
if [ $# -eq 0 ]; then # sin parámetros no hay nada que mirar
echo "Uso: $0 archivo.c [archivo.c ...]" >&2
exit 2
fi
problemas=0
for f in "$@"; do # "$@": cada parámetro entero, aunque tenga espacios
if [ ! -f "$f" ]; then
echo "$f: no existe"
problemas=$((problemas + 1))
continue
fi
lineas=$(wc -l < "$f") # con < wc solo escribe el número, sin el nombre
if grep -q "int main" "$f"; then # -q: callado; if solo mira el código de grep
echo "$f: $lineas líneas, tiene main"
else
echo "$f: $lineas líneas, SIN main"
problemas=$((problemas + 1))
fi
done
[ "$problemas" -eq 0 ] # 0 si todos existen y tienen main
Por qué está hecho así:
for f in "$@": es la única forma de quepractica final.cllegue entero. Con$*o$@sin comillas, el bucle intentaría mirarpracticayfinal.cpor separado, y dos de los resultados diríanno existe.[ $# -eq 0 ]al principio: sin archivos no hay nada que hacer, así que mensaje de uso por stderr yexit 2antes de entrar en el bucle.continueen el caso de que no exista: avisa, cuenta el problema y pasa al siguiente. No tiene sentido contar las líneas de un archivo que no existe.wc -l < "$f": con<,wclee de su entrada y no sabe cómo se llama el archivo, así que solo escribe el número. Conwc -l "$f"saldría8 entregas/ana.cy habría que recortarlo.if grep -q "int main" "$f":grepdevuelve 0 si encuentra el texto y 1 si no. La-qhace que no escriba la línea encontrada. Es elifde la teoría: no compara nada, mira un código.- El código final sale de
[ "$problemas" -eq 0 ]. En la segunda llamada hay 2 problemas (practica final.csinmainynadie.c, que no existe), así que devuelve 1. En la tercera, las cinco entregas tienenmain: 0. Las líneas cuadran: los cuatro programas tienen 8 y el de sergio 9, por suint x;de más.
Sí, el script escribe «1 líneas». Arreglarlo es un buen extra: un if [ "$lineas" -eq 1 ] que elija entre «línea» y «líneas».
Solución del nivel 2
#!/bin/bash
# notas.sh: lee "alumno nota" de un archivo, pone la calificación y calcula la media
# Uso: ./notas.sh archivo
[ $# -eq 1 ] || { echo "Uso: $0 archivo" >&2; exit 2; }
[ -f "$1" ] || { echo "No existe: $1" >&2; exit 1; }
suma=0
n=0
aprobados=0
raras=0
while read -r alumno nota; do # read reparte la línea: 1.ª palabra y el resto
case "$nota" in
[0-9]|10) ;; # un dígito o 10: es una nota válida, seguimos
*)
echo "$alumno: nota no válida ($nota)"
raras=$((raras + 1))
continue ;;
esac
if [ "$nota" -ge 9 ]; then # -ge y no >: comparamos números
palabra="Sobresaliente"
elif [ "$nota" -ge 7 ]; then
palabra="Notable"
elif [ "$nota" -ge 5 ]; then
palabra="Aprobado"
else
palabra="Suspenso"
fi
echo "$alumno: $nota $palabra"
suma=$((suma + nota))
n=$((n + 1))
if [ "$nota" -ge 5 ]; then
aprobados=$((aprobados + 1))
fi
done < "$1"
echo "Notas válidas: $n. Aprobados: $aprobados. No válidas: $raras"
echo "Media: $((suma / n)).$(( (suma * 10 / n) % 10 ))" # bash solo hace cuentas con enteros
[ "$raras" -eq 0 ]
Por qué está hecho así:
[ $# -eq 1 ] || { …; exit 2; }: las llaves agrupan varias órdenes para que el||ejecute las dos. Es unifde una línea, muy habitual al principio de los scripts.read -r alumno nota: con dos variables, la primera palabra va aalumnoy el resto anota. Si alguna línea tuviera tres palabras,notase quedaría con las dos últimas y elcasela rechazaría.- El
caseva antes de cualquier-ge: siNPllegara a[ "$nota" -ge 9 ], saldríainteger expression expected(lo viste consieteen la teoría). Primero se valida y después se compara.[0-9]|10)encaja con un solo dígito o con10;11,-3oNPvan al*). -gey no>: comparamos números. Con>dentro de[ ]crearíamos archivos con nombre de nota.- La media: notas válidas 10 + 5 + 10 + 0 + 10 = 35, y 35 / 5 = 7. La parte entera es
$((35 / 5))= 7 y el decimal,(35 × 10 / 5) % 10= 70 % 10 = 0. De ahí7.0. Si la suma fuera 36: 36 / 5 = 7 y 360 / 5 = 72, 72 % 10 = 2, o sea7.2, que es 36 / 5 = 7,2. - Los aprobados: ana, dani, laura y sergio tienen 5 o más; pablo no. 4 aprobados, y
martano cuenta porque su nota no es válida. - El
exitfinal es la comparación[ "$raras" -eq 0 ]: hay una nota rara, así que devuelve 1. Connotas.csv, que no existe, la segunda comprobación corta antes de leer nada y también devuelve 1.
Dividir entre n sin pensar en el caso n = 0. Si el archivo no tuviera ninguna nota válida, $((suma / n)) daría division by 0. Un if [ "$n" -eq 0 ] antes de la media lo evita.
Solución del nivel 3
#!/bin/bash
# revisar_lista.sh: corrige a todos los de la lista, hayan entregado o no
# Uso: ./revisar_lista.sh lista carpeta
if [ $# -ne 2 ]; then
echo "Uso: $0 lista carpeta" >&2
exit 2
fi
lista=$1
carpeta=$2
nota_de() { # nota_de ALUMNO: escribe su nota (o el motivo)
local fuente="$carpeta/$1.c"
local binario="$carpeta/$1"
if [ ! -f "$fuente" ]; then
echo "NO ENTREGADO"
return 1
fi
rm -f "$binario"
if ! gcc -Wall "$fuente" -o "$binario" 2> /dev/null; then
echo "0"
return 0
fi
if [ "$("$binario" < entrada.txt)" = "$(cat esperado.txt)" ]; then
echo "10"
else
echo "5"
fi
}
en_lista=0
entregadas=0
correctas=0
while read -r alumno; do
en_lista=$((en_lista + 1))
nota=$(nota_de "$alumno") # $( ) recoge lo que la función escribe
if [ $? -eq 0 ]; then # justo después: el $? es el de nota_de
entregadas=$((entregadas + 1))
fi
if [ "$nota" = "10" ]; then
correctas=$((correctas + 1))
fi
printf "%-8s %s\n" "$alumno" "$nota" # %-8s: nombre a la izquierda en 8 columnas
done < "$lista"
echo "En la lista: $en_lista. Entregadas: $entregadas. Correctas: $correctas"
[ "$entregadas" -eq "$en_lista" ]
Por qué está hecho así:
- La función escribe y devuelve cosas distintas.
echomanda la nota a stdout (la recoge$( )) yreturn 1marca el caso «no entregado». Así, con una sola llamada sabemos la nota y si cuenta como entregada. if [ $? -eq 0 ]va justo después denota=$(nota_de …): una asignación con$( )deja en$?el código de lo que se ejecutó dentro. Si pusiéramos algo en medio, perderíamos el código (el fragmento 3).return 0cuando no compila: el alumno sí ha entregado, aunque se lleve un 0. SoloNO ENTREGADOdevuelve 1.rm -fantes de compilar, por la misma razón que en la práctica: que un ejecutable viejo no apruebe por nadie.printf "%-8s %s\n":%-8sescribe el nombre a la izquierda y rellena con espacios hasta 8 columnas.echono sabe alinear.- Las cuentas: 7 alumnos en la lista; entregan 5 (todos menos marta y ruben, 7 − 2 = 5); correctas las de 10: ana, laura y sergio, 3. Como 5 ≠ 7, la última comparación devuelve 1.
Pruébalo en tu terminal
La trampa del fragmento 2, en directo y sin script, para que la veas en tres órdenes. Tecléalo en una carpeta vacía:
$ correctas=2 $ [ $correctas > 3 ] && echo "mayor que 3" mayor que 3 $ ls 3 $ [ "$correctas" -gt 3 ] && echo "mayor que 3" || echo "no es mayor que 3" no es mayor que 3 $ rm 3 $ [[ $correctas > 10 ]] && echo "cadena: 2 va después de 10" cadena: 2 va después de 10
Qué observar:
[ $correctas > 3 ] && echo "mayor que 3"escribemayor que 3aunque 2 no es mayor que 3.- El
lssiguiente enseña el archivo3recién creado: es la prueba de que el>ha redirigido. - Con
-gt, la misma comparación sale falsa y el||escribeno es mayor que 3. - La última línea es la trampa de
[[ ]]: ahí>compara cadenas, y"2"va después de"10"en el orden del diccionario (el2va después del1), así que «2 > 10» sale verdadero.
Chuletario: shell scripts en Linux
Lo esencial en 30 segundos
- Primera línea
#!/bin/bash,chmod +xuna vez y./script.sh. - Variables sin espacios en el
=y siempre entre comillas dobles al usarlas:"$x". - Parámetros:
$1…$#, y"$@"para recorrerlos. ifmira el código de salida (0 = bien).[ ]es una orden: espacios dentro.- Números con
-eq -ne -lt -le -gt -gey$(( )); cadenas con=y!=. - Errores por stderr (
>&2) yexitcon un código que signifique algo.
Quiero… / Código / Resultado
| Quiero… | Código | Resultado |
|---|---|---|
| Guardar la salida de una orden | n=$(ls entregas | wc -l) | n vale 5 |
| Usar una variable pegada a texto | "${alumno}_v2.c" | laura_v2.c |
| Un parámetro con valor por defecto | carpeta=${1:-entregas} | entregas si no hay $1 |
| Quitar la extensión | basename entregas/ana.c .c o ${f%.c} | ana / entregas/ana |
| Comprobar el número de parámetros | [ $# -eq 1 ] || uso | uso si no hay exactamente 1 |
| Saber si un archivo existe | [ -f "$f" ] | código 0 si existe |
| Saber si un archivo tiene algo | [ -s "$f" ] | código 0 si no está vacío |
| Buscar un texto sin escribirlo | grep -q "int main" "$f" | código 0 si lo encuentra |
| Comparar dos archivos | cmp -s a b | código 0 si son idénticos |
| Hacer una cuenta | $((suma / n)) | entero: 7 / 2 = 3 |
| Leer un archivo línea a línea | while read -r a b; do … done < f | a = 1.ª palabra, b = resto |
| Quitar la opción ya leída | shift | $2 pasa a ser $1 |
| Alinear columnas | printf "%-8s %s\n" "$a" "$b" | nombre en 8 columnas |
| Ver qué ejecuta el script | bash -x script.sh | cada orden con + |
| Comprobar la sintaxis | bash -n script.sh | código 0 si está bien |
Plantillas de los ejercicios
| Ejercicio | Esqueleto | Resultado con nuestros datos |
|---|---|---|
| Nivel 1 | for f in "$@"; do [ -f "$f" ] || continue; …; done | 5 entregas con main, código 0 |
| Nivel 2 | while read -r a n; do case "$n" in [0-9]|10) ;; *) continue ;; esac; …; done < "$1" | Media 7.0, 4 aprobados, código 1 |
| Nivel 3 | nota=$(nota_de "$a"); if [ $? -eq 0 ]; then …; fi | 7 en la lista, 5 entregadas, 3 correctas, código 1 |
Síntoma / Causa probable
| Síntoma | Causa probable |
|---|---|
Permission denied al lanzar ./script.sh | Falta chmod +x |
script.sh: command not found | Falta el ./ delante |
x: command not found en una asignación | Espacios alrededor del = |
[1: command not found | Falta el espacio después de [ |
| Aparece un archivo con nombre de número | > o < dentro de [ ] |
integer expression expected | -eq, -gt… con algo que no es un número |
| Un nombre con espacio se parte en dos | Variable sin comillas o $* en lugar de "$@" |
if decide al revés | $? mirado tarde, o un exit 1 cuando todo fue bien |
$? da 0 tras una tubería que falló | Es el código de la última orden de la tubería (PIPESTATUS) |
| El script se queda parado | Un programa o un read esperan datos del teclado: falta un < |
Antes de entregar, pregúntate
- ¿Comprueba el script sus parámetros al principio y termina con
exit 2y un mensaje por stderr si lo llaman mal? - ¿Están todas las variables entre comillas dobles y recorres los parámetros con
"$@"? - ¿Comparas números con
-eq,-gt… y cadenas con=? - ¿Devuelve el script 0 solo cuando todo ha ido bien? Compruébalo con
echo $?después de cada caso.
Con esto cierras los shell scripts de IC2. Si quieres repasar, vuelve a la teoría o a la práctica. Lo siguiente del Linux de IC2 será vi y la configuración de red.
Sigue aprendiendoTodos los artículos de Linux¿Te ha servido este artículo? Así puedes apoyar SergioLearns →




Qué es un sistema operativo — teoría con tu portátil en una tarde de estudio, una llamada a write() y la CPU que nunca descansa (y los tres programas que no se ejecutaban a la vez)