Scripts Bash

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda
La versión para imprimir ya no se admite y puede contener errores de representación. Actualiza los marcadores del navegador y utiliza en su lugar la función de impresión predeterminada del navegador.

Estructura de un script

La estructura básica de un script de Bash es:

 #!/bin/bash
 # Indicamos que se debe ejecutar con el shell bash
 
 # comandos del script
 # Tenemos a nuestra disposición las variables $1, $2.. $*, $#, $@, $0
 
 exit 0
 # Con esta salida indicamos a otro posible script que lo invoque que ha terminado correctamente.

Redirecciones

$ cmd > fichero

Redirige la salida estandar a un fichero.

$ cmd 2> fichero

Redirige la salida de error a un fichero.

$ cmd >> fichero

Añade a un fichero la salida estandar sin borrar el contenido anterior.

$ cmd 2>> fichero

Añade los errores al final de un fichero.

$ cmd &> fichero

Redirige la salida estandar y la de error a un fichero.

$ cmd > fichero 2>&1
Ojo con
$ cmd 2>&1 >>fichero
Porque no funcionará, ya que evalúa de izquierda a derecha. Es decir, primero envía el error a la salida estándar 1 que es la terminal y luego la salida estándar la modifica por el fichero. Pero ya ha salido el error por pantalla

Otra manera de redirigir las dos salidas a un fichero. En este caso, la estándar va al fichero y la de error va a la estándar.

$ cmd > /dev/null
$ cmd 2> /dev/null
$ cmd &> /dev/null

Descartar la salida estándar, la de error o las dos.

$ cmd < fichero

Redirige el contenido de un fichero a la entrada estandar del comando.

$ cmd << FIN 
linea1 
linea2 
FIN 

Redirige una serie de líneas a un comando. Esto se llama Here-Document. La palabra FIN sólo es para indicar el final de las líneas y puede ser cualquier otra.

$ cmd <<< "palabra"

Redirecciona el texto al comando. Se llama here-string.

$ exec 2> fichero

Redirige la salida de error a un fichero para todos los comandos. El comando exec es uno de los llamados built-in de bash. Permite ejecutar un comando sustituyéndo al shell actual o, los que nos interesa ahora, gestionar las redirecciones.

$ exec 3< fichero

Abre un fichero para leer usando un nuevo descriptor.

$ exec 3> fichero 

Abre un fichero para escribir usando un nuevo descriptor.

$ exec 3<> fichero 

Abre un fichero para leer o escribir usando el descriptor 3.

$ exec 3>&-

Cierra un descriptor de fichero.

 # open it
 exec 3< input.txt
 
 # for example: read one line from the file(-descriptor)
 read -u 3 LINE   # la opción -u en read permite seleccionar el descriptor de fichero
 # or
 read LINE <&3
 
 # finally, close it
 exec 3<&-

Si queremos unir las salidas de varios comandos seguidos, podemos usar las {}:

ERROR=$( { ./useless.sh | sed s/Output/Useless/ > outfile; } 2>&1 )

http://mywiki.wooledge.org/BashFAQ/032

read interactivo de ficheros

$ seq 30 > numeros

Observemos este script:

 #!/bin/bash 
 # cada 10 linies espera a que l'usuari escriga alguna cosa
 
 while read Num 
 do 
 	let ContLin++ # Contando... 
 	echo -n "$Num " # -n para no saltar línea 
 	((ContLin % 10)) > /dev/null || read 
 done < numeros

El resultado es este:

1 2 3 4 5 6 7 8 9 10 12 13 14 15 16 17 18 19 20 21 23 24 25 26 27 28 29 30

Y no se espera a que el usuario ponga nada.

Esto es porque el read de dentro del bucle lee también del fichero. Se puede forzar a que lea de la terminal:

 #!/bin/bash 
 # cada 10 linies espera a que l'usuari escriga alguna cosa
 
 while read Num 
 do 
 	let ContLin++ # Contando... 
 	echo -n "$Num " # -n para no saltar línea 
 	((ContLin % 10)) > /dev/null || read '''< /dev/tty'''
 done < numeros

O se puede crear un descriptor de fichero y modificar el read del while:

 #!/bin/bash 
 # cada 10 linies espera a que l'usuari escriga alguna cosa
 
 exec 3< numeros
 while read Num '''<&3'''
 do 
 	let ContLin++ # Contando... 
 	echo -n "$Num " # -n para no saltar línea 
 	((ContLin % 10)) > /dev/null || read
 done

Process Substitution

Los comandos que necesitan un fichero, pueden ejecutarse con el |, por ejemplo el wc puede hacerse:

$ cat /etc/passwd | wc -l

A veces un comando acepta como entrada dos ficheros. En ese caso, el ejemplo anterior no se puede seguir. Para solucionarse, se pueden usar los () después de una < [1]

$ diff <(ls $first_directory) <(ls $second_directory)
Obsérvese que delante de los paréntesis no hay un $, si lo hubiera, el resultado lo pasaría literal, como una variable. De esta manera, el resultado de los ls lo encapsula en una especie fichero virtual temporal y así lo entiende el comando diff. Esto se puede ver si se lo pasamos al echo. Da un fichero situado en /dev/fd/ donde, casualmente, hay otros con el nombre de 1, 2, 255 ... esos son los descriptores de la salida estándar o de error. Los definidos por exec también se ven ahí.

Parámetros, variables y argumentos

Un parámetro es una entidad referenciada por su nombre, un número o un carácter especial.

  • Si tiene nombre se llama Variable
  • Si es un número se considera parámetros posicionales y tienen que ver con los argumentos que se le pasa a una función o script.
    • $0: Nombre del script.
    • $1,$2,$3...$9 Argumentos. En principio no se puede poner más de 9 argumentos. Si se quiere poner 10 o más, se puede recurrir al shift o poniéndolo como ${12}
  • Si son caracteres especiales tienen diversos significados:
  • $# Candidad de argumentos
  • $* Todos los argumentos. Es equivalente a $@ si se ponen sin dobles comillas. Con dobles comillas, $* son todos los argumentos seguidos y como separador el primer caracter del $IFS y $@ son todos los argumentos separados.
$ function f(){ IFS='-'; echo $*; echo "$*"; echo $@; echo "$@";}; f 2 3 4 5 "6 7"
  • $? : a 0 si el comando anterior ha terminado con éxito. A 1 o más si ha dado erroro a terminado sin éxito. Es el Exit Status
  • $_ : El último argumento del comando anterior
  • $$ : El PID del proceso actual
  • $! : El PID del último proceso ejecutado en background.
$ sleep 1000 & ps; echo -e "\nProceso actual: $$\nProceso en background: $!"; 
  • $- : La lista de opciones del proceso actual.


shift

Este comando permite usar un mismo número de argumento para ir recorriéndolos.

En este ejemplo se ve bastante bien. Via

 while test -n "$1"; do
    case "$1" in
        -a)
            opciona=$2
            shift
            ;;
        -b)
            opcionb=$2
            shift
            ;;
        -c)
            opcionc=$2
            shift
            ;;
        -d)
            opciond=$2
            shift
            ;;
        *)
            echo "Unknown argument: $1"
            exit 0
            ;;
    esac
    shift
 done

El while recorre todos los argumentos, aunque sólo comprueba el $1 en cada iteración, se queda con $2 y pasa al siguiente. De esta manera, el órden de los argumentos y sus opciones no es relevante.

# miscript -a opciona -b opcionb -d opciond
# miscript -b opcionb -c opcionc

Interpretación de los argumentos por el shell

El shell se encarga de sustituir los caracteres de sustitución y las variables antes de pasar los argumentos a un comando o script. Esto se puede ver con este script:

 #!/bin/bash 
 
 echo $*
 echo $#

Si se invoca de estas maneras, por ejemplo:

$ ./argumentos *
$ ./argumentos p[a-z]
$ ./argumentos {1..100}
$ ./argumentos $HOME

A esto se le llama Expansión de parámetros. Se pueden hacer muchas cosas manipulando esta expansión:


Uso simple:

  • $PARAMETER
  • ${PARAMETER}

Indirección:

  • ${!PARAMETER}
$ a=100
$ b=a
$ echo "$b=${!b}"

Modificación de mayúsculas:

  • ${PARAMETER^} # La primera
  • ${PARAMETER^^} # todas
  • ${PARAMETER,} # la primera en minúscula
  • ${PARAMETER,,} # todas
  • ${PARAMETER~} # invertir
  • ${PARAMETER~~}

Expansión del nombre de la variable:

  • ${!PREFIX*} # todas las variables que tengan ese prefijo
  • ${!PREFIX@}

Quitar substrings:

  • ${PARAMETER#PATTERN} #Quita el prefijo más corto que cumpla el patrón
  • ${PARAMETER##PATTERN} # Quita el prefijo más largo que cumpla el patrón
  • ${PARAMETER%PATTERN} # Quita sufijo más corto
  • ${PARAMETER%%PATTERN} # quita el sufijo más largo

Buscar y reemplazar

  • ${PARAMETER/PATTERN/STRING} # Una vez
  • ${PARAMETER//PATTERN/STRING} # Todas las ocurrencias
  • ${PARAMETER/PATTERN} # Lo quita una vez
  • ${PARAMETER//PATTERN} # Lo quita todas las ocurrencias

Longitud de la cadena

  • ${#PARAMETER}

Substrings:

  • ${PARAMETER:OFFSET}
  • ${PARAMETER:OFFSET:LENGTH}

Usar valor por defecto

  • ${PARAMETER:-WORD} # Si es nulo o vacío
  • ${PARAMETER-WORD} # Si es nulo. Si la variable está vacía no imprime el valor por defecto.

Asignar valor por defecto

  • ${PARAMETER:=WORD} # Si es nulo o vacío.
  • ${PARAMETER=WORD} # Sólo asigna valor si es nulo.

Valor alternativo

  • ${PARAMETER:+WORD} # Si está declarada y con contenido da un valor alternativo.
  • ${PARAMETER+WORD} # Da un valor alternativo sólo si está declarada aunque su contenido sea nulo .

Error si no está declarado

  • ${PARAMETER:?WORD} # Error si no está declarado o es nulo.
  • ${PARAMETER?WORD} # error si no está declarado, si es nulo no da error.

Arrays

Ver Arrays en Bash

Tests

El [ para hacer test se puede usar en todos los shell que cumplen con POSIX. Mientras que el doble [[ está en bash y otros shells modernos.

#POSIX
[ "$variable" ] || echo 'variable is unset or empty!' >&2
[ -f "$filename" ] || printf 'File does not exist: %s\n' "$filename" >&2

Saber más:

$ whereis [
[: /usr/bin/[ /usr/share/man/man1/[.1.gz
$ man [

Los dobles [[ no son un comando, sino que forman parte del bash.

Funciones

Función Expresión Ejemplo
Comparación de strings < , > , = , == !=
 [[ a > b ]] || echo "a does not come before b" 
 [[ az < za ]] && echo "az comes before za"
Comparación de integers -ge , -gt , -lt , -le , -eq , -ne
 [[ 6 -ne 20 ]] && echo "6 is not equal to 20"
Condicional || , &&
 [[ -n $var && -f $var ]] && echo "$var is a file"
Agrupar (...) (obsoleto)
 [[ $var = img* && ($var = *.png || $var = *.jpg) ]] && echo "$var starts with img and ends with .jpg or .png"
Patrones = o ==
 [[ $name = a* ]] || echo "name does not start with an 'a': $name"
Expresión regular =~
 [[ $(date) =~ ^Fri\ ...\ 13 ]] && echo "It's Friday the 13th"
Negación !
 [[ ! -u $file ]] && echo "$file is not a setuid file"

Ejemplos con ficheros:

[[ -e $config ]] && echo "config file exists: $config"
[[ $file0 -nt $file1 ]] && echo "$file0 is newer than $file1"
[[ $input -ef $output ]] && { echo "will not overwrite input file: $input"; exit 1; }

Se recomienda usar [[ si el nombre de los ficheros puede tener espacios en blanco:

file="file name"
 [[ -f $file ]] && echo "$file is a file"


Como norma general se recomienda usar [[ para ficheros y cadenas. Para números se puede usar expresiones aritméticas con(())

via:[2]

Expresiones aritméticas

Bash sólo puede trabajar con enteros. Si quieres cálculos en coma flotante puedes usar bc

El comando típico para hacer operaciones matemáticas es let

let a=17+23
let a="17 + 23"

Pero es más útil usar (()), ya que puedes usar un $ delante $(()). Permite usar espacios en blanco y no necesita usar $ en las variables porque no están permitidas cadenas dentro.

((a=$a+7))         # Add 7 to a
((a = a + 7))      # Add 7 to a.  Identical to the previous command.
((a += 7))         # Add 7 to a.  Identical to the previous command.

((a = RANDOM % 10 + 1))     # Choose a random number from 1 to 10.
                           # % is modulus, as in C.

# (( )) may also be used as a command.  > or < inside (( )) means
# greater/less than, not output/input redirection.
if ((a > 5)); then echo "a is more than 5"; fi

Los (()) sin $ delante sólo va en bash. Los siguientes ejemplos son compatibles con todos los POSIX

a=$((a+7))         # POSIX-compatible version of previous code.
if test $((a%4)) = 0; then ...

Las expressions aritmèticas tambien aceptan resultados condicionales:

echo $((2<3?4:5))

No se recomienda usar $[...] porque se considera obsoleta.

via: [3]

if

El if en bash lo que hace es evaluar la variable $? resultante de la ejecución de un comando. Por tanto, el if se puede poner antes de cualquier comando.

$? es una variable que guarda un 0 si el comando anterior se ha ejecutado con éxito (no es lo mismo que sin errores) y un 1 o más si ha finalizado sin éxito, ya sea por un error o porque no ha dado resultado

Ejecuta esto después de un comando exitoso o no exitoso:

 $ echo -e ":\\0$(($??50:51))";

Para hacer un if tradicional como en C que evalúa una expresión lógica se puede hacer con los comandos [] o [[]]. El [ es un comando que tiene hasta manual. Es otra forma del comando test, pero más parecido sintácticamente a otros lenguajes de programación.


Así, se pueden hacer muchos if:

$ if test 1 -le 3; then echo "1 es menor que 3"; fi
$ if [ 1 -le 3 ]; then echo "1 es menor que 3"; fi
$ if [[ 1 -le 3 ]]; then echo "1 es menor que 3"; fi
$ if grep lliurex /etc/passwd > /dev/null; then echo "El usuario Lliurex existe"; fi

En ocasiones se puede evitar el if, uniendo comandos con && o ||. En el caso de &&, el segundo comando sólo se ejecuta si el primero ha acabado con éxito. En el caso de || sólo se ejecuta si el primero ha terminado sin éxito.

Estos son los comandos anteriores sin if:

$ test 1 -le 3 && echo "1 es menor que 3"
$ [ 1 -le 3 ] && echo "1 es menor que 3"
$ [[ 1 -le 3 ]] && echo "1 es menor que 3"
$ grep lliurex /etc/passwd > /dev/null && echo "El usuario Lliurex existe"

while

El comportamiento es igual que el if, evalúa el éxito del comando que va a continuación.

for

Este comando se comporta de manera distinta a lenguajes como C. El for de bash sólo recorre una lista que le has pasado. Por ejemplo:

$ for i in 1 2 3; do echo $i; done

Si queremos un rango más ámplio se puede usar {..}:

$ for i in {1..1000}; do echo $i; done

Si queremos aumentar la i de 2 en 2, por ejemplo:

$ for i in $(seq 1 2 1000); do echo $i; done

Si necesitamos una sintaxis parecida a C:

$ for ((i=0;i<10;i++)); do echo $i; done

Funciones

ftp://ftp.monash.edu.au/pub/linux/docs/LDP/abs/html/functions.html http://blog.joncairns.com/2013/08/what-you-need-to-know-about-bash-functions/

Enlaces

http://mywiki.wooledge.org/BashGuide/Practices

http://tldp.org/LDP/abs/html/

http://www.pixelbeat.org/programming/shell_script_mistakes.html

http://serverfault.com/questions/7503/how-to-determine-if-a-bash-variable-is-empty

http://www.kfirlavi.com/blog/2012/11/14/defensive-bash-programming

http://tldp.org/LDP/abs/html/writingscripts.html

http://mywiki.wooledge.org/BashPitfalls

https://lucasfcosta.com/2019/04/07/streams-introduction.html