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.
Tabla de Contenidos
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:
echoescribe un texto.$USERes 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:
./hola.shsin la x →Permission denied. El archivo se crea con-rw-r--r--(lo viste en Permisos en Linux: laumask022 nunca da x a un archivo nuevo).bash hola.shfunciona aunque no haya x. Aquí no ejecutas el archivo: ejecutasbashy le pasas el archivo para que lo lea. Para leer basta con lar.chmod +x hola.shpone la x para dueño, grupo y otros (-rwxr-xr-x). A partir de ahí,./hola.shfunciona: el sistema abre el archivo, ve#!/bin/bashen la primera línea y le pasa el archivo a/bin/bash.hola.shsin./→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/biny/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:
| Orden | Qué pasa | Por qué |
|---|---|---|
alumno=laura | Crea la variable | Sin espacios alrededor del = |
echo "Corrigiendo a $alumno" | Corrigiendo a laura | Dentro de comillas dobles, $alumno se sustituye |
echo 'Corrigiendo a $alumno' | Corrigiendo a $alumno | Dentro de comillas simples, nada se sustituye: el texto va tal cual |
alumno = laura | alumno: command not found | Con espacios, bash entiende «ejecuta la orden alumno con los parámetros = y laura» |
echo "Archivo: $alumno_v2.c" | Archivo: .c | Bash 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.c | Las 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 $*:
$0es 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 rompepractica final.cen 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.
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: ejecutaorden2solo siorden1ha devuelto 0 (laura compila).orden1 || orden2: ejecutaorden2solo siorden1ha 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.
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 alumnolee una línea entera y la guarda enalumno. La-rhace 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 aalumnoy todo lo demás anota. Lo usarás en los ejercicios. readdevuelve 0 si ha podido leer y 1 si se ha acabado la entrada. Por esowhile readsirve 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):-gtcompara 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: el1va antes que el9, 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… | Operador | Ejemplo |
|---|---|---|
| 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:
| Vuelta | f | n después | Escribe |
|---|---|---|---|
| 1 | entregas/ana.c | 1 | 1: ana.c |
| 2 | entregas/dani.c | 2 | 2: dani.c |
| 3 | entregas/laura.c | 3 | 3: laura.c |
| 4 | entregas/pablo.c | 4 | 4: pablo.c |
| 5 | entregas/sergio.c | 5 | 5: 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-xescribe el error y termina conexit 2, que vemos conecho $?.
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
compilarantes de definirla,command not found. $1dentro de la función es el archivo que le pasas (entregas/ana.c), aunque el script haya recibidoholacomo$1.localhace quefuenteybinariosolo existan dentro de la función. Por eso la última línea escribe'': fuera,fuenteno existe. Sinlocal, la variable se quedaría «pegada» y podría pisar otra del mismo nombre.returndevuelve un código (0-255), no un texto.if compilar "$f"mira ese código igual que miraría el degcc. Para que una función «devuelva» un texto, la función lo escribe conechoy 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:
- El here-document (
<< 'EOF') crea el archivo sin abrir ningún editor. Las comillas de'EOF'hacen que$1y$#se guarden tal cual en el archivo, sin sustituirse al crearlo. - El primer
./saludo.sh LauradaPermission denied: un archivo recién creado nunca tiene x. Después dechmod +x, funciona. ./saludo.sh Ana Ruizson dos parámetros:$1es soloAnay$#vale 2. Con comillas,"Ana Ruiz"es uno.echo $?da0: el script no tieneexit, así que devuelve el código de su última orden, elecho, que ha ido bien.- Cambia el script para que termine con
exit 3y vuelve a mirarecho $?. Así compruebas que el código lo decides tú.
Resumen
| Concepto | Cómo se escribe | Lo que no hay que olvidar |
|---|---|---|
| Primera línea | #!/bin/bash | En la línea 1, sin espacios delante |
| Ejecutar | chmod +x s.sh y ./s.sh | Sin ./ se busca en $PATH |
| Variable | x=5, "$x", ${x}_v2 | Sin espacios en el =; usar entre comillas dobles |
| Salida de una orden | n=$(ls | wc -l) | $( ) ejecuta y sustituye |
| Parámetros | $0, $1, $#, "$@" | "$@" respeta los espacios; $* no |
| Código de salida | $?, exit 2, return 1 | 0 = bien; mirar $? justo después |
| Leer | read -r x | Devuelve 1 cuando se acaba la entrada |
| Condición | if orden; then … fi, [ "$n" -gt 3 ] | if mira un código; [ ] necesita espacios |
| Cuentas | $((n + 1)) | Solo enteros: 7 / 2 = 3 |
| Bucles | for f in dir/*.c, while read -r l; do … done < f | El < va en el done |
| Elegir | case "$1" in -c) … ;; *) … ;; esac | * va la última |
| Funciones | f() { local x=$1; …; return 0; } | Definir antes de usar; return ≠ exit |
Tres ideas para quedarte
- En bash, los espacios significan algo. Separan una orden de sus parámetros: por eso
x = 5falla,[ ]los necesita y las variables van entre comillas. - 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. - Bash no se para cuando algo falla. Avisa y sigue. Por eso un buen script comprueba sus parámetros al principio y termina con
exiten 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 →




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)