Shell 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)
La segunda semana del corrector de mi grupo de IC2 pasó algo raro. Pablo me dijo: «oye, tu script dice que mi práctica compila, pero a mí me da error». Y tenía razón: su .c no compilaba. ¿Qué había pasado? Que el día anterior sí había compilado una versión antigua, el ejecutable pablo seguía en la carpeta y mi script, en lugar de mirar si gcc había ido bien, miraba si existía el ejecutable. Estaba corrigiendo el pasado.
La lección se me quedó grabada: en un script, lo que manda es el código de salida de la orden, no lo que crees que ha dejado en la carpeta. Y, de paso, borra lo viejo antes de generar lo nuevo.
Esta práctica pone en marcha todo lo de la teoría de shell scripts con tres programas de nivel medio que, juntos, forman un corrector de prácticas completo: compilar.sh (compila todas las entregas y separa los errores), probar.sh (ejecuta cada una con una entrada fija y compara su salida) y corregir.sh (el corrector final, con opciones, funciones y nota). Los datos se parecen a los de verdad: cinco entregas de la práctica «suma» (leer dos enteros y escribir su suma), y entre ellas hay una que no compila, otra que compila con un aviso y otra que compila pero da un resultado equivocado.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Cómo se lanza un script:
#!/bin/bash,chmod +xy./script.sh. - Que
ifmira el código de salida de una orden (0 = bien) y que[ ]es una orden más, con espacios. - Los parámetros
$1,$#y"$@", y por qué las variables van entre comillas. - Las redirecciones de Redirecciones y tuberías:
>,2>,<,/dev/nully|.
Si alguno te suena dudoso, repasa la teoría antes de seguir.
Monta una carpeta de pruebas con una subcarpeta entregas/ y cinco .c (los cinco son el mismo programa con pequeños cambios: dani.c multiplica en vez de sumar, a pablo.c le falta el ; del printf y sergio.c declara un int x; que no usa). Añade entrada.txt con 3 4 y esperado.txt con 7. Las sesiones de este artículo se han ejecutado una sola vez como el usuario sergio, con LANG=C: por eso los mensajes de gcc y de bash salen en inglés.
Programa 1: compilar todas las entregas
compilar.sh recibe una carpeta, compila cada .c con gcc -Wall y escribe una línea por alumno: OK, OK (con avisos) o ERROR. Los mensajes de gcc no se pierden: van a un archivo por alumno en errores/. Lo que lo complica: hay que comprobar que nos han pasado bien la carpeta, distinguir los avisos de los errores y que el script devuelva un código útil para quien lo use.
La idea
El orden de los pasos es el de cualquier script serio:
- Comprobar los parámetros al principio. Si no hay exactamente uno, o no es una carpeta, mensaje por stderr y
exit 2. Mejor parar al principio que fallar a medias. - Recorrer las entregas con
for fuente in "$carpeta"/*.c. - Para cada una: sacar el nombre del alumno con
basename, borrar el ejecutable viejo y compilar, mandando stderr aerrores/<alumno>.txt. - Decidir con el código de
gcc(no mirando si existe el ejecutable): si es 0, compila; si además el archivo de errores no está vacío, es que hubo avisos. - Contar, escribir el resumen y terminar con un código: 0 si todo compiló, 1 si no.
Antes de programar, piensa en qué datos necesitas fuera del bucle (los contadores ok y mal, que empiezan en 0) y cuáles cambian en cada vuelta (fuente, alumno, binario).
El código
#!/bin/bash
# compilar.sh: compila todas las entregas de una carpeta y dice cuáles compilan
# Uso: ./compilar.sh carpeta
if [ $# -ne 1 ]; then # exactamente un parámetro: la carpeta
echo "Uso: $0 carpeta" >&2 # los mensajes de error van por stderr
exit 2 # 2 = me has llamado mal
fi
carpeta=$1
if [ ! -d "$carpeta" ]; then # -d: existe y es una carpeta
echo "No es una carpeta: $carpeta" >&2
exit 2
fi
mkdir -p errores # -p: no falla si ya existe
ok=0
mal=0
for fuente in "$carpeta"/*.c; do # comillas en la variable, el * fuera
alumno=$(basename "$fuente" .c) # entregas/ana.c -> ana
binario="$carpeta/$alumno"
rm -f "$binario" # sin esto, un ejecutable viejo se haría pasar por bueno
if gcc -Wall "$fuente" -o "$binario" 2> "errores/$alumno.txt"; then
ok=$((ok + 1))
if [ -s "errores/$alumno.txt" ]; then # -s: existe y NO está vacío
echo "OK $alumno (con avisos)"
else
echo "OK $alumno"
fi
else
mal=$((mal + 1))
echo "ERROR $alumno"
fi
done
echo "Compilan: $ok. No compilan: $mal"
[ "$mal" -eq 0 ] # última orden = código del script: 0 si todo compiló
El corazón del script es el bucle. Vamos a verlo de cerca:
for fuente in "$carpeta"/*.c; do # comillas en la variable, el * fuera
alumno=$(basename "$fuente" .c) # entregas/ana.c -> ana
binario="$carpeta/$alumno"
rm -f "$binario" # sin esto, un ejecutable viejo se haría pasar por bueno
if gcc -Wall "$fuente" -o "$binario" 2> "errores/$alumno.txt"; then
ok=$((ok + 1))
if [ -s "errores/$alumno.txt" ]; then # -s: existe y NO está vacío
echo "OK $alumno (con avisos)"
else
echo "OK $alumno"
fi
else
mal=$((mal + 1))
echo "ERROR $alumno"
fi
done
"$carpeta"/*.c: la variable va entre comillas (por si la carpeta tiene espacios) y el*fuera, porque dentro de comillas bash no lo expandiría.basename "$fuente" .cquita la carpeta y, con el segundo parámetro, también la extensión:entregas/ana.c→ana.rm -f "$binario"es el arreglo de mi anécdota. La-fhace quermno se queje si el archivo no existe (la primera vez nunca existe).if gcc … 2> "errores/$alumno.txt"; then: compila y, a la vez, guarda los mensajes. Elifmira el código degcc.[ -s "errores/$alumno.txt" ]:-ses verdadero si el archivo existe y no está vacío. Sigcccompiló pero escribió algo, son avisos.
Esta es la traza del bucle con nuestras cinco entregas:
| Vuelta | alumno | $? de gcc | ¿errores vacío? | Escribe | ok | mal |
|---|---|---|---|---|---|---|
| 1 | ana | 0 | sí | OK ana | 1 | 0 |
| 2 | dani | 0 | sí | OK dani | 2 | 0 |
| 3 | laura | 0 | sí | OK laura | 3 | 0 |
| 4 | pablo | 1 | — | ERROR pablo | 3 | 1 |
| 5 | sergio | 0 | no | OK sergio (con avisos) | 4 | 1 |
Y la última línea, [ "$mal" -eq 0 ], no escribe nada: es la última orden del script, así que su código es el código del script. Con mal = 1, devuelve 1.
Salida
$ chmod +x compilar.sh
$ ./compilar.sh
Uso: ./compilar.sh carpeta
$ echo $?
2
$ ./compilar.sh entregass
No es una carpeta: entregass
$ echo $?
2
$ ./compilar.sh entregas
OK ana
OK dani
OK laura
ERROR pablo
OK sergio (con avisos)
Compilan: 4. No compilan: 1
$ echo $?
1
$ ls entregas
ana
ana.c
dani
dani.c
laura
laura.c
pablo.c
sergio
sergio.c
$ ls errores
ana.txt
dani.txt
laura.txt
pablo.txt
sergio.txt
$ cat errores/pablo.txt
entregas/pablo.c: In function 'main':
entregas/pablo.c:6:26: error: expected ';' before 'return'
6 | printf("%d\n", a + b)
| ^
| ;
7 | return 0;
| ~~~~~~
$ head -n 2 errores/sergio.txt
entregas/sergio.c: In function 'main':
entregas/sergio.c:5:9: warning: unused variable 'x' [-Wunused-variable]
Comprobémoslo a mano. Sin parámetros: mensaje de uso y código 2. Con entregass (mal escrito): no es una carpeta, código 2 otra vez. Con entregas: compilan 4 (ana, dani, laura y sergio) y falla 1 (pablo), 4 + 1 = 5 entregas, y el script devuelve 1 porque mal no es 0.
ls entregas confirma que hay cuatro ejecutables nuevos y que no hay pablo: el rm -f lo habría borrado aunque existiera de antes. En errores/ hay un archivo por alumno (los de ana, dani y laura, vacíos). El de pablo cuenta exactamente qué falla: falta un ; al final de la línea 6. El de sergio es un warning: la variable x no se usa. Compila, pero -Wall lo avisa, y nuestro script lo marca como OK (con avisos).
Dani sale OK aunque su programa esté mal (multiplica en vez de sumar). Compilar no es lo mismo que funcionar. De eso se encarga el programa 2.
Preguntas que te puedes hacer
¿Por qué los mensajes de uso van con >&2? Porque son errores, no resultados. Si alguien hace ./compilar.sh entregas > resultado.txt, en el archivo solo quiere las líneas de OK y ERROR; los avisos de uso tienen que verse en la pantalla. Es la separación stdout/stderr que viste en Redirecciones.
¿Por qué exit 2 y no exit 1? Es una convención muy extendida: 2 suele significar «me has llamado mal» (lo hacen ls, grep, bash -n…) y 1, «he trabajado, pero algo ha fallado». Así, quien use el script puede distinguir un fallo de uso de una entrega que no compila.
¿Por qué no se mira si existe el ejecutable? Por la anécdota del principio: un ejecutable viejo puede seguir ahí. El código de salida de gcc dice lo que ha pasado en esta ejecución, que es lo único que nos interesa. Y el rm -f es la segunda red de seguridad.
¿Qué pasa si la carpeta no tiene ningún .c? Bash deja el patrón tal cual: el bucle da una vuelta con fuente valiendo literalmente entregas/*.c, y gcc falla. Se arregla poniendo al principio del bucle [ -f "$fuente" ] || continue (o con shopt -s nullglob, que hace que un patrón sin coincidencias se convierta en nada).
¿Por qué el resumen dice «No compilan: 1» y no se para en el primer error? Porque un corrector tiene que corregir a todos. Un error de pablo no es motivo para no compilar a sergio. Lo que sí hacemos es recordarlo en mal y reflejarlo en el código de salida al final.
Escribir for fuente in "$carpeta/*.c", con el * dentro de las comillas. Bash no expande comodines dentro de comillas, así que el bucle da una sola vuelta con el texto entregas/*.c. Las comillas, solo alrededor de la variable.
Para ver qué hace un script línea a línea, lánzalo con bash -x compilar.sh entregas. Cada orden aparece con un + delante, ya con las variables sustituidas. Lo verás en la sección de la terminal.
Pruébalo tú
- Añade una opción
-q(quiet) que no escriba nada y solo devuelva el código de salida, para usarlo en unif. - Haz que, al final, el script escriba la primera línea de cada archivo de errores que no esté vacío (pista:
head -n 1dentro de unforsobreerrores/*.txt).
Programa 2: probar las salidas
probar.sh va un paso más allá: ejecuta cada entrega con la entrada de entrada.txt (3 4) y compara lo que escribe con esperado.txt (7). Para cada alumno dice CORRECTO, SALIDA DISTINTA (y qué ha dado) o SIN EJECUTABLE. Lo que lo complica: la comprobación de cada alumno tiene tres resultados posibles, no dos, y queremos tenerla en una función.
La idea
La función probar hace el trabajo y devuelve un número; el bucle principal convierte ese número en texto con un case:
- 2 si no hay ejecutable (pablo no compiló).
- Si lo hay, lo ejecuta con
< entrada.txty guarda lo que escribe ensalidas/<alumno>.txt. - Después compara con
cmp -s, que devuelve 0 si los dos archivos son idénticos y 1 si no. Como es la última orden de la función, su código es el de la función.
Separar «calcular» de «escribir» hace la función reutilizable: el programa 3 podría usarla sin cambiarle nada.
Antes de programar, piensa en el orden de los pasos: no se puede probar lo que no se ha compilado. Por eso la sesión lanza primero compilar.sh.
El código
#!/bin/bash
# probar.sh: ejecuta cada entrega con entrada.txt y compara con esperado.txt
# Uso: ./probar.sh [carpeta] (después de ./compilar.sh carpeta)
carpeta=${1:-entregas} # si no me dan carpeta, uso entregas
probar() { # probar ALUMNO -> 0 bien, 1 salida distinta, 2 sin ejecutable
local alumno=$1
local binario="$carpeta/$alumno"
if [ ! -x "$binario" ]; then # -x: existe y se puede ejecutar
return 2
fi
"$binario" < entrada.txt > "salidas/$alumno.txt"
cmp -s "salidas/$alumno.txt" esperado.txt # -s: sin mensajes; solo importa su $?
}
mkdir -p salidas
for fuente in "$carpeta"/*.c; do
alumno=$(basename "$fuente" .c)
probar "$alumno"
case $? in # case mira el código que devolvió probar
0) echo "CORRECTO $alumno" ;;
1) echo "SALIDA DISTINTA $alumno: da $(cat "salidas/$alumno.txt"), se esperaba $(cat esperado.txt)" ;;
2) echo "SIN EJECUTABLE $alumno" ;;
esac
done
Lo más nuevo está en la función:
probar() { # probar ALUMNO -> 0 bien, 1 salida distinta, 2 sin ejecutable
local alumno=$1
local binario="$carpeta/$alumno"
if [ ! -x "$binario" ]; then # -x: existe y se puede ejecutar
return 2
fi
"$binario" < entrada.txt > "salidas/$alumno.txt"
cmp -s "salidas/$alumno.txt" esperado.txt # -s: sin mensajes; solo importa su $?
}
carpeta=${1:-entregas}(línea 5 del script):${1:-valor}significa «$1, y si está vacío,valor». Así el parámetro es opcional.[ ! -x "$binario" ]:-xcomprueba que existe y se puede ejecutar. Si no,return 2y la función termina ahí."$binario" < entrada.txt > "salidas/$alumno.txt": la entrada sale del archivo (elscanfdel programa lee3 4) y la salida va a otro archivo. Son las redirecciones de siempre, pero sobre un programa nuestro.cmp -s: compara byte a byte; la-s(silent) hace que no escriba nada. Solo nos interesa su código.
Así va la traza de la función para cada alumno:
alumno | ¿-x? | Escribe en salidas/ | cmp -s | Devuelve | El case escribe |
|---|---|---|---|---|---|
| ana | sí | 7 | iguales | 0 | CORRECTO |
| dani | sí | 12 | distintos | 1 | SALIDA DISTINTA |
| laura | sí | 7 | iguales | 0 | CORRECTO |
| pablo | no | — | — | 2 | SIN EJECUTABLE |
| sergio | sí | 7 | iguales | 0 | CORRECTO |
Salida
$ chmod +x compilar.sh probar.sh $ ./compilar.sh entregas > /dev/null $ ./probar.sh entregas CORRECTO ana SALIDA DISTINTA dani: da 12, se esperaba 7 CORRECTO laura SIN EJECUTABLE pablo CORRECTO sergio $ cat salidas/dani.txt 12 $ ls salidas ana.txt dani.txt laura.txt sergio.txt
La primera orden compila con > /dev/null: no queremos ver otra vez sus OK, solo que deje los ejecutables preparados. Después, las cuentas: 3 + 4 = 7, y eso es lo que dan ana, laura y sergio. Dani da 3 × 4 = 12, porque multiplica. Pablo no tiene ejecutable, así que ni se intenta. En salidas/ hay cuatro archivos, no cinco: el de pablo no llega a crearse, porque la función hace return 2 antes de ejecutar nada.
Preguntas que te puedes hacer
¿Por qué cmp -s y no comparar con [ "$(cat a)" = "$(cat b)" ]? Las dos formas valen aquí. cmp compara los archivos exactos (incluidos los saltos de línea del final), mientras que $( ) quita los saltos de línea finales. Para un corrector estricto, cmp; para uno que perdone un salto de línea de más, la comparación de cadenas. En el programa 3 usamos la segunda.
¿Por qué el case $? va justo después de probar "$alumno"? Porque $? es el código de la última orden. Si en medio hubiera un echo, el case vería el 0 del echo y todos saldrían CORRECTO.
¿Por qué local? Porque alumno también existe en el bucle principal. Sin local, la función usaría la misma variable; aquí no rompería nada (tienen el mismo valor), pero es un error esperando a pasar el día que cambies algo. Con local, la función no toca nada de fuera.
¿Qué pasa si un alumno entrega un programa que se queda esperando para siempre? El corrector se queda colgado con él. En un corrector real se usa timeout 5 "$binario" < entrada.txt, que lo mata a los 5 segundos y devuelve 124. Sería un cuarto caso para el case.
Olvidar el < entrada.txt. El programa del alumno se queda esperando a que escribas los números por teclado y parece que el script se ha colgado. Cuando un script ejecuta otros programas, decide siempre de dónde leen.
diff salidas/dani.txt esperado.txt te enseña en qué se diferencian dos archivos, línea a línea. Es lo que usarías para dar al alumno una explicación del fallo.
Pruébalo tú
- Añade el caso del
timeout: si el programa tarda más de 2 segundos, que la función devuelva 3 y elcaseescribaDEMASIADO LENTO. - Haz que el script pruebe con varias entradas:
entrada1.txt/esperado1.txt,entrada2.txt/esperado2.txt…, y diga cuántas pasa cada alumno.
Programa 3: el corrector completo
corregir.sh junta todo: compila, prueba y pone nota (10 si la salida es correcta, 5 si compila pero da otra cosa, 0 si no compila). Además acepta una opción: -c para solo compilar y -p para compilar y probar (lo que hace si no le dices nada). Lo que lo complica: las opciones van antes de la carpeta, así que la carpeta puede estar en $1 o en $2, y el código de salida tiene que decir si todas las entregas están bien.
La idea
- Una función
usocon el mensaje de error yexit 2, para no repetirlo tres veces. - Una función
compilarmuy corta, cuyo código de salida es el degcc. - Un
case "$1"para la opción. Si hay opción,shiftla quita y la carpeta pasa a ser$1. Así, el resto del script no tiene que preguntar si había opción o no. - El bucle usa
continuepara saltar al siguiente alumno en cuanto ya sabe su nota. - La última orden compara
correctascontotalpara dar el código del script.
Antes de programar, escribe en papel las llamadas que tienen que funcionar (./corregir.sh entregas, ./corregir.sh -c entregas, ./corregir.sh -p entregas) y las que tienen que dar error (./corregir.sh, ./corregir.sh -x entregas, ./corregir.sh a b).
El código
#!/bin/bash
# corregir.sh: el corrector completo de la práctica "suma"
# Uso: ./corregir.sh [-c | -p] carpeta
# -c solo compila -p compila y prueba (lo que hace si no le dices nada)
uso() { # una función para no repetir el mensaje tres veces
echo "Uso: $0 [-c | -p] carpeta" >&2
exit 2 # exit dentro de una función termina TODO el script
}
compilar() { # compilar FUENTE BINARIO: devuelve el código de gcc
gcc -Wall "$1" -o "$2" 2> /dev/null
}
modo=probar
case "$1" in
-c) modo=compilar; shift ;; # shift: tira $1 y el resto se mueve ($2 pasa a $1)
-p) shift ;;
-*) uso ;; # cualquier otra opción que empiece por -
esac
[ $# -eq 1 ] || uso # || : si la comprobación falla, uso
[ -d "$1" ] || uso
carpeta=$1
correctas=0
total=0
for fuente in "$carpeta"/*.c; do
alumno=$(basename "$fuente" .c)
binario="$carpeta/$alumno"
total=$((total + 1))
rm -f "$binario"
if ! compilar "$fuente" "$binario"; then
echo "$alumno: 0 (no compila)"
continue # a por el siguiente alumno
fi
if [ "$modo" = compilar ]; then # = compara CADENAS
echo "$alumno: compila"
correctas=$((correctas + 1))
continue
fi
salida=$("$binario" < entrada.txt)
if [ "$salida" = "$(cat esperado.txt)" ]; then
echo "$alumno: 10"
correctas=$((correctas + 1))
else
echo "$alumno: 5 (compila, pero da $salida)"
fi
done
echo "Correctas: $correctas de $total"
[ "$correctas" -eq "$total" ] # código de salida: 0 solo si todas están bien
La parte que más cuesta es la de las opciones:
modo=probar
case "$1" in
-c) modo=compilar; shift ;; # shift: tira $1 y el resto se mueve ($2 pasa a $1)
-p) shift ;;
-*) uso ;; # cualquier otra opción que empiece por -
esac
[ $# -eq 1 ] || uso # || : si la comprobación falla, uso
[ -d "$1" ] || uso
carpeta=$1
modo=probares el valor por defecto: si no hay opción, nada lo cambia.-c)cambia el modo y haceshift.-p)solo haceshift, porque el modo ya eraprobar.-*)encaja con cualquier otra cosa que empiece por-: es una opción que no conocemos, así queuso. Va después de-cy-pporque en uncasegana el primer patrón que encaja.- Si
$1no empieza por-(por ejemplo,entregas), ninguna rama encaja y no pasa nada. [ $# -eq 1 ] || uso: «o hay exactamente un parámetro, o uso». Se lee igual que unifde una línea.
Fíjate en que uso termina con exit 2, no con return: si la llamada está mal, no tiene sentido seguir, y exit dentro de una función termina el script entero.
En el bucle, salida=$("$binario" < entrada.txt) guarda lo que escribe el programa del alumno, y [ "$salida" = "$(cat esperado.txt)" ] compara cadenas con =. La traza en modo probar:
| Vuelta | alumno | ¿Compila? | salida | Nota | correctas | total |
|---|---|---|---|---|---|---|
| 1 | ana | sí | 7 | 10 | 1 | 1 |
| 2 | dani | sí | 12 | 5 | 1 | 2 |
| 3 | laura | sí | 7 | 10 | 2 | 3 |
| 4 | pablo | no | — | 0 | 2 | 4 |
| 5 | sergio | sí | 7 | 10 | 3 | 5 |
Salida
$ chmod +x corregir.sh $ ./corregir.sh -x entregas Uso: ./corregir.sh [-c | -p] carpeta $ echo $? 2 $ ./corregir.sh -c entregas ana: compila dani: compila laura: compila pablo: 0 (no compila) sergio: compila Correctas: 4 de 5 $ ./corregir.sh entregas | tee informe.txt ana: 10 dani: 5 (compila, pero da 12) laura: 10 pablo: 0 (no compila) sergio: 10 Correctas: 3 de 5 $ echo $? 0 $ ./corregir.sh entregas > /dev/null $ echo $? 1 $ ./corregir.sh entregas > /dev/null && echo "Todo correcto" || echo "Hay que revisar" Hay que revisar $ wc -l informe.txt 6 informe.txt
Comprobémoslo. Con -x: opción desconocida, uso y código 2. Con -c: solo compila, así que cuenta como correctas las 4 que compilan (Correctas: 4 de 5). Sin opción: notas 10, 5, 10, 0 y 10, y 3 correctas de 5, que es lo que dice la traza.
Ahora, el detalle más interesante: después de ./corregir.sh entregas | tee informe.txt, echo $? da 0, aunque el corrector haya terminado con 1. ¿Por qué? Porque en una tubería, $? es el código de la última orden, y la última es tee, que ha guardado el informe sin problemas. En cuanto lo lanzamos sin tubería (> /dev/null), echo $? da el 1 de verdad, y el && … || escribe Hay que revisar. Por último, wc -l informe.txt cuenta 6 líneas: 5 alumnos + 1 resumen.
Si necesitas el código de una orden que está en medio de una tubería, bash lo guarda en el array PIPESTATUS: justo después de ./corregir.sh entregas | tee informe.txt, echo "${PIPESTATUS[0]}" daría 1.
Preguntas que te puedes hacer
¿Por qué shift y no mirar $2 cuando hay opción? Porque entonces todo el script tendría que saber si había opción («¿la carpeta está en $1 o en $2?»). Con shift, a partir de la línea 21 la carpeta siempre está en $1 y $# siempre debe valer 1.
¿Por qué if ! compilar …? El ! invierte el código: entra en el then cuando compilar falla. Así el caso raro (no compila) se resuelve primero y con continue, y el resto del bucle se queda sin anidar.
¿Por qué [ "$modo" = compilar ] con = y no con -eq? Porque modo es una palabra. -eq solo sirve para números; con palabras daría integer expression expected, como el siete de la teoría.
¿Por qué dani tiene un 5 y no un 0? Es una decisión de corrección: su programa compila y hace algo, solo que no lo correcto. El script lo refleja con una nota intermedia y, además, dice qué ha dado (da 12) para que se pueda revisar a mano.
¿Por qué el código de salida compara correctas con total? Para que el script se pueda usar desde otro script o desde un if sin leer el texto: if ./corregir.sh entregas > /dev/null; then echo "todo bien"; fi.
Fiarte del $? después de una tubería. ./corregir.sh entregas | tee informe.txt siempre parece ir bien, porque $? es el de tee. Si el código importa, no lo metas en una tubería o usa PIPESTATUS.
shift se puede usar en un while [ $# -gt 0 ] para procesar varias opciones seguidas (-c -v entregas): en cada vuelta miras $1 con un case y haces shift.
Pruébalo tú
- Añade una opción
-o archivoque guarde el informe en ese archivo (pista: después de la opción viene el nombre, así que necesitarásshift 2). - Haz que la nota de los que compilan con avisos baje un punto (pista: guarda los errores en un archivo en lugar de en
/dev/nully comprueba con-s).
Errores típicos en esta práctica
- Comillas con el comodín dentro (
"$carpeta/*.c"): el*no se expande y el bucle da una sola vuelta con un nombre que no existe. - Decidir por la existencia del ejecutable: un ejecutable viejo hace pasar por buena una entrega que ya no compila. Usa el código de
gccy borra lo viejo conrm -f. - Ejecutar un programa sin
<: se queda esperando al teclado y el script parece colgado. - Mirar
$?tarde o después de una tubería: lo que ves es el código de otra orden (elechoo eltee). - Comparar palabras con
-eq(o números con=):-eq,-lt… para números;=,!=para cadenas. - Usar
returnen una función que tiene que cortar el script (comouso):returnsolo sale de la función, y el script seguiría con datos incorrectos.
Pruébalo en tu terminal
Dos herramientas para cuando un script no hace lo que esperas: bash -x, que enseña cada orden antes de ejecutarla, y bash -n, que comprueba la sintaxis sin ejecutar nada. Teclea esto en la carpeta de pruebas:
$ cat > mini.sh << 'EOF' > #!/bin/bash > for f in entregas/ana.c entregas/pablo.c; do > gcc "$f" -o prog 2> /dev/null && echo "OK $f" || echo "ERROR $f" > done > EOF $ bash -x mini.sh + for f in entregas/ana.c entregas/pablo.c + gcc entregas/ana.c -o prog + echo 'OK entregas/ana.c' OK entregas/ana.c + for f in entregas/ana.c entregas/pablo.c + gcc entregas/pablo.c -o prog + echo 'ERROR entregas/pablo.c' ERROR entregas/pablo.c $ cat > roto.sh << 'EOF' > #!/bin/bash > if [ -f entregas/ana.c ]; then > echo "ana ha entregado" > EOF $ bash -n roto.sh roto.sh: line 4: syntax error: unexpected end of file $ echo $? 2
Qué observar:
- Con
bash -x, cada orden sale con un+delante y con las variables ya sustituidas: vesgcc entregas/ana.c -o prog, nogcc "$f" -o prog. - En la vuelta de pablo, después de
gccviene directamenteecho 'ERROR …': el&&se ha saltado elOKporquegccdevolvió 1, y el||ha ejecutado su parte. - Las líneas con
+van por stderr: si hacesbash -x mini.sh 2> traza.txt, la salida normal sale por pantalla y la traza, al archivo. bash -n roto.shencuentra que falta elfi(unexpected end of file, en la línea 4, que es donde se acaba el archivo) y devuelve2, sin haber ejecutado elecho. Es lo primero que conviene hacer con un script nuevo.
Resumen
- Un script serio comprueba sus parámetros al principio y termina con
exit 2y un mensaje por stderr si lo llaman mal. - Las decisiones se toman con códigos de salida (
if gcc …,cmp -s,[ -s archivo ]), no mirando qué ha quedado en la carpeta; y lo viejo se borra antes de generar lo nuevo. - Una función que devuelve un número con
returny uncase $?que lo traduce a texto separan el cálculo de la presentación. shiftdeja los parámetros siempre en el mismo sitio después de procesar las opciones.- El script tiene que devolver un código que signifique algo (0 = todo bien), y ojo con
$?después de una tubería. bash -npara la sintaxis ybash -xpara ver qué hace de verdad, línea a línea.
Checklist: ¿lo has practicado?
- ¿Has ejecutado los tres programas y comprobado las salidas?
- ¿Sabes explicar por qué
pablono aparece enls entregasaunque hubiera compilado otro día? - ¿Puedes decir qué devuelve la función
probarpara cada alumno y por qué? - ¿Entiendes por qué
echo $?da 0 después de| tee informe.txt? - ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
- ¿Has usado
bash -xal menos una vez para seguir uno de tus scripts?
Si has respondido que sí a casi todo, estás listo para los ejercicios de shell scripts, donde ya no te doy el código hecho. ¡Nos vemos allí!
Siguiente paso: ejerciciosShell 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)¿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)