viernes, 30 de junio de 2017

Operaciones con cadenas

Hay una gran colección de expansiones que pueden usarse para operar con cadenas. Muchas de estas expansiones son particularmente adecuadas para operaciones con rutas.

${#parámetro}

se expande en la longitud de la cadena contenida en parámetro. Normalmente, parámetro es una cadena; sin embargo, si parámetro es @ o *, la expansión da como resultado el número de parámetros posicionales.

[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo "'$foo' is ${#foo} characters long."
'This string is long.' is 20 characters long.



${parámetro:margen}
${parámetro:margen:longitud}

Estas expansiones se usan para extraer una porción de la cadena contenida en parámetro. La extracción comienza en margen caracteres desde el principio de la cadena y continúa hasta el final de la cadena, a no ser que se especifique longitud.

[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo:5}
string is long.
[me@linuxbox ~]$ echo ${foo:5:6}
string

Si el valor de margen es negativo, se considera que empieza por el final de la cadena en lugar de por el principio. Fíjate que los valores negativos deben ir precedidos por un espacio para evitar la confusión con la expansión ${parámetro:-palabra}. longitud, si está presente, no debe ser menor de cero.

Si parámetro es @, el resultado de la expansión es longitud parámetros posicionales, comenzando en margen.

[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo: -5}
long.
[me@linuxbox ~]$ echo ${foo: -5:2}
lo



${parámetro#patrón}
${parámetro##patrón}

Estas expansiones eliminan la parte delantera de la cadena contenida en parámetro definida por patrón. patrón es un patrón comodín como los que se usan en expansiones de rutas. La diferencia entre las dos formas es que la # elimina el resultado más corto, mientras que la forma ## elimina el resultado más largo.

[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo#*.}
txt.zip
[me@linuxbox ~]$ echo ${foo##*.}
zip



${parámetro%patrón}
${parámetro%%patrón}

Estas expansiones son iguales que las # y ## anteriores, excepto que eliminan texto desde el final de la cadena contenida en parámetro en lugar que desde el principio.

[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo%.*}
file.txt
[me@linuxbox ~]$ echo ${foo%%.*}
file



${parámetro/patrón/cadena}
${parámetro//patrón/cadena}
${parámetro/#patrón/cadena}
${parámetro/%patrón/cadena}

Esta expansión realiza un "buscar y reemplazar" dentro del contenido de parámetro. Si se encuentra texto que coincida con el comodín patrón, se reemplaza con el contenido de cadena. En la forma normal, solo se reemplaza la primera coincidencia de patrón. En la forma //, se reemplazan todas las coincidencias. La forma /# requiere que la coincidencia ocurra al principio de la cadena, y la forma /% requiere que la coincidencia ocurra al final de la cadena. /cadena puede omitirse, lo que causa que el texto señalado por patrón se borre.

[me@linuxbox ~]$ foo=JPG.JPG
[me@linuxbox ~]$ echo ${foo/JPG/jpg}
jpg.JPG
[me@linuxbox ~]$ echo ${foo//JPG/jpg}
jpg.jpg
[me@linuxbox ~]$ echo ${foo/#JPG/jpg}
jpg.JPG
[me@linuxbox ~]$ echo ${foo/%JPG/jpg}
JPG.jpg

Es bueno saber la expansión de parámetros. Las expansiones con manipulación de cadenas pueden usarse como sustitutos de otros comandos comunes como sed o cut. Las expansiones mejoran la eficiencia de los scripts eliminando el uso de programas externos. Como ejemplo, modificaremos el programa longest-word que vimos en el capítulo anterior para usar la expansión de parámetros ${#j} en lugar de la sustitución de comandos $(echo $j | wc -c) y su subshell resultante, así:

#!/bin/bash

# longest-word3 : find longest string in a file

for i; do
    if [[ -r $i ]]; then
        max_word=
        max_len=
        for j in $(strings $i); do
            len=${#j}
            if (( len > max_len )); then
                max_len=$len
                max_word=$j
            fi
        done
        echo "$i: '$max_word' ($max_len characters)"
    fi
    shift
done

A continuación, compararemos la eficiencia de las dos versiones usando el comando time:

[me@linuxbox ~]$ time longest-word2 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list' (38 characters)

real 0m3.618s
user 0m1.544s
sys 0m1.768s
[me@linuxbox ~]$ time longest-word3 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list' (38 characters)

real 0m0.060s
user 0m0.056s
sys 0m0.008s

La versión original del script tarda 3,618 segundos en escanear el archivo, mientras que la nueva versión, usando expansión de parámetros, tarda sólo 0,06 segundos - una mejora significativa.

jueves, 29 de junio de 2017

Expansiones que devuelven nombres de variables

El shell tiene la capacidad de devolver los nombres de las variables. Esto se usa en algunas situaciones algo exóticas.

${!prefijo*}
${!prefijo@}

Esta expansión devuelve los nombres de variables existentes con nombres que empiecen con prefijo. Según la documentación de bash, ambas formas de expansión se comportan idénticamente. Aquí, listamos todas las variables en el entorno con nombres que comiencen con BASH:

[me@linuxbox ~]$ echo ${!BASH*}
BASH BASH_ARGC BASH_ARGV BASH_COMMAND BASH_COMPLETION
BASH_COMPLETION_DIR BASH_LINENO BASH_SOURCE BASH_SUBSHELL
BASH_VERSINFO BASH_VERSION

miércoles, 28 de junio de 2017

Expansiones para manejar variables vacías

Varias expansiones de parámetros manejan variables inexistentes o vacías. Estas expansiones son útiles para manejar parámetros posicionales perdidos y asignar valores por defecto a parámetros.

${parámetro:-palabra}

Si parámetro está sin definir (es decir, no existe) o está vacío, esta expansión tiene como resultado el valor de palabra. Si parámetro no está vacío, la expansión tiene como resultado el valor de parámetro.

[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
substitute value if unset
[me@linuxbox ~]$ echo $foo

[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar



${parámetro:=palabra}

Si parámetro está sin definir o vacío, esta expansión tiene como resultado el valor de palabra. Además, el valor de palabra se asigna a parámetro. Si parámetro no está vacío, la expansión tiene como resultado el valor de parámetro.

[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
default value if unset
[me@linuxbox ~]$ echo $foo
default value if unset
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar

Nota: Los posicionales y otros parámetros especiales no pueden asignarse de esta forma.

${parámetro:?palabra}

Si parámetro no está establecido o está vacío, esta expansión hace que el script termine con un error, y el contenido de palabra se envíe al error estándar. Si parámetro no está vacío, la expansión da como resultado el valor de parámetro.

[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bash: foo: parameter is empty
[me@linuxbox ~]$ echo $?
1
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bar
[me@linuxbox ~]$ echo $?
0



${parámetro:+palabra}

Si parámetro no está establecido o está vacío, la expansión no genera nada. Si parámetro no está vacío, el valor de palabra se sustituye por parámetro; sin embargo, el valor de parámetro no cambia.

[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}

[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}
substitute value if set

martes, 27 de junio de 2017

Parámetros básicos

La forma más simple de expansión de parámetros se refleja en el uso ordinario de variables. Por ejemplo:

$a

cuando se expande, se convierte en lo que contenga la variable a. Los parámetros simples también pueden incluirse entre llaves:

${a}

Esto no tiene efecto en la expansión, pero se requiere si la variable es adyacente a otro texto, que pueda confundir al shell. En este ejemplo, intentaremos crear un nombre de archivo añadiendo la cadena "_file" al contenido de la variable a.

[me@linuxbox ~]$ a="foo"
[me@linuxbox ~]$ echo "$a_file"

Si ejecutamos esta secuencia, el resultado será nada, porque el shell intentará expandir la variable a_file en lugar de a. Este problema puede solucionarse añadiendo llaves:

[me@linuxbox ~]$ echo "${a}_file"
foo_file

También hemos visto que podemos acceder a los parámetros posicionales mayores de 9 incluyendo el número entre llaves. Por ejemplo, para acceder al parámetro posicional undécimo, podemos hacer esto:

${11}

lunes, 26 de junio de 2017

Expansión de parámetros

Aunque la expansión de parámetros surgió en el Capítulo 7, no lo vimos en detalle porque la mayoría de las expansiones de parámetros se usan en scripts en lugar de en la línea de comandos. Ya hemos trabajado con algunas formas de expansión de parámetros; por ejemplo, las variables de shell. El shell ofrece muchas más.

viernes, 23 de junio de 2017

Cadenas y números

Todos los programas de ordenador trabajan con datos. En los capítulos anteriores, nos hemos enfocado en procesamiento de datos a nivel de archivo. Sin embargo, muchos problemas de programación necesitan solventarse usando unidades de datos más pequeñas como cadenas y números.

En este capítulo, veremos varias funcionalidades del shell que se usan para manejar cadenas y números. El shell proporciona una variedad de expansiones de parámetros que realizan operaciones con cadenas. Además de la expansión aritmética (que vimos en el Capítulo 7), hay un programa de línea de comandos muy común llamado bc, que realiza matemáticas de alto nivel.

jueves, 22 de junio de 2017

Para saber más

miércoles, 21 de junio de 2017

Resumiendo

Con nuestro conocimiento del comando for, aplicaremos ahora las mejoras finales a nuestro script sys_info_page. Actualmente, la función report_home_space aparece así:

report_home_space () {
    if [[ $(id -u) -eq 0 ]]; then
        cat <<- _EOF_
            <H2>Home Space Utilization (All Users)</H2>
            <PRE>$(du -sh /home/*)</PRE>
            _EOF_
    else
        cat <<- _EOF_
            <H2>Home Space Utilization ($USER)</H2>
            <PRE>$(du -sh $HOME)</PRE>
            _EOF_
    fi
    return
}

A continuación, la reescribiremos para proporcionar más detalle para cada directorio home de usuario, e incluiremos el número total de archivos y subdirectorios en cada uno de ellos:

report_home_space () {

  local format="%8s%10s%10s\n"
  local i dir_list total_files total_dirs total_size user_name

  if [[ $(id -u) -eq 0 ]]; then
      dir_list=/home/*
      user_name="All Users"
  else
      dir_list=$HOME
      user_name=$USER
  fi

  echo "<H2>Home Space Utilization ($user_name)</H2>"

  for i in $dir_list; do

      total_files=$(find $i -type f | wc -l)
      total_dirs=$(find $i -type d | wc -l)
      total_size=$(du -sh $i | cut -f 1)

      echo "<H3>$i</H3>"
      echo "<PRE>"
      printf "$format" "Dirs" "Files" "Size"
      printf "$format" "----" "-----" "----"
      printf "$format" $total_dirs $total_files $total_size
      echo "</PRE>"
  done
  return
}

Esta reescritura aplica mucho de lo que hemos aprendido hasta ahora. Aún probamos el superusuario, pero en lugar de realizar la lista completa de acciones como parte del if, configuramos algunas variables usadas posteriormente en un bucle for. Hemos añadido varias variables locales a la función y hecho uso de printf para formatear parte de la salida.

martes, 20 de junio de 2017

for: Forma del lenguaje C

Versiones recientes de bash han añadido una segunda forma de la sintaxis del comando for, una que se parece a la forma que encontramos en el lenguaje de programación C. Muchos otros lenguajes también soportan esta forma:

for (( expresión1; expresión2; expresión3 )); do
    comandos
done

donde expresión1, expresión2 y expresión3 son expresiones aritméticas y comandos son los comandos a ejecutar durante cada iteración del bucle.

En términos de comportamiento, esta forma es equivalente a la siguiente estructura:

(( expresión1 ))
while (( expresión2 )); do
    comandos
    (( expresión3 ))
done

expresión1 se usa para inicializar condiciones para el bucle, expresión2 se usa para determinar cuándo termina el bucle y expresión3 se ejecuta al final de cada iteración del bucle.

Aquí tenemos una aplicación típica:

#!/bin/bash

# simple_counter : demo of C style for command

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

Cuando lo ejecutamos, produce la siguiente salida:

[me@linuxbox ~]$ simple_counter
0
1
2
3
4

En este ejemplo, expresión1 inicializa la variable i con el valor cero, expresión2 permite al bucle continuar mientras que el valor de i permanezca menor que 5, y expresión3 incrementa el valor de i en uno cada vez que se repite el bucle.

La forma del lenguaje C de for es útil siempre que necesitemos una secuencia numérica. Veremos varias aplicaciones de esto en los siguientes dos capítulos.

lunes, 19 de junio de 2017

¿Por qué i?

Habrás notado que se ha elegido la variable i para cada uno de los ejemplos anteriores del bucle for. ¿Por qué? No hay una razón específica en realidad, sólo la tradición. La variable utilizada con for puede ser cualquier variable válida, pero i es la más común, seguida de j y k.

El origen de esta tradición viene del lenguaje de programación Fortran. En Fortran, las variables no declaradas que comiencen con las letras I, J, K, L y M son automáticamente tratadas como enteros, mientras que las variables que comiencen con cualquier otra letra se tratan como reales (números con fracciones decimales). Este comportamiento llevó a los programadores a usar las variables I, J y K para variables de bucles, ya que era menos trabajoso usarlas cuando se necesitaba una variable temporal (y las variables de bucles lo son a menudo).

También llevó al siguiente chiste basado en Fortran:

"DIOS es real, a menos que se le declare como entero".

viernes, 16 de junio de 2017

for: Forma tradicional del shell

La sintaxis original del comando for es:

for variable [in palabras]; do
    comandos
done

Donde variable es el nombre de la variable que se incrementará durante la ejecución del bucle, palabras es una lista opcional de elementos que se asignarán secuencialmente a variable, y comandos son los comandos que se van a ejecutar en cada iteración del bucle.

El comando for es útil en la línea de comandos. Podemos comprobar fácilmente cómo funciona:

[me@linuxbox ~]$ for i in A B C D; do echo $i; done
A
B
C
D

En este ejemplo, se le da a for una lista de cuatro palabras: "A", "B", "C" y "D". Con una lista de cuatro palabras, el bucle se ejecuta cuatro veces. Cada vez que se ejecuta el bucle, se asigna una palabra a la variable i, Dentro del bucle, tenemos un comando echo que muestra el valor de i para mostrar la asignación. Al igual que los bucles while y until, la palabra done cierra el bucle.

La característica realmente potente de for es el número de formas interesantes de crear la lista de palabras. Por ejemplo, a través de expansión con llaves:

[me@linuxbox ~]$ for i in {A..D}; do echo $i; done
A
B
C
D

or expansión de rutas:

[me@linuxbox ~]$ for i in distros*.txt; do echo $i; done
distros-by-date.txt
distros-dates.txt
distros-key-names.txt
distros-key-vernums.txt
distros-names.txt
distros.txt
distros-vernums.txt
distros-versions.txt

o sustitución de comandos:

#!/bin/bash

# longest-word : find longest string in a file

while [[ -n $1 ]]; do
    if [[ -r $1 ]]; then
        max_word=
        max_len=0
        for i in $(strings $1); do
            len=$(echo $i | wc -c)
            if (( len > max_len )); then
                max_len=$len
                max_word=$i
            fi
        done
        echo "$1: '$max_word' ($max_len characters)"
    fi
    shift
done

En este ejemplo, buscamos la cadena más larga dentro de una archivo. Una vez que le damos uno o más nombres de archivo en la línea de comandos, este programa usa el programa strings (que está incluido en el paquete GNU binutils) para generar una lista de "words" en texto legible en cada archivo. El bucle for procesa, de una en una, cada palabra y determina si la palabra actual es la más larga encontrada hasta el momento. Cuando concluye el bucle, se muestra la palabra más larga.

Si la parte opcional in words del comando for se omite, for procesa por defecto los parámetros posicionales. Modificaremos nuestro script longest-word para usar este método:

#!/bin/bash

# longest-word2 : find longest string in a file

for i; do
    if [[ -r $i ]]; then
        max_word=
        max_len=0
        for j in $(strings $i); do
            len=$(echo $j | wc -c)
            if (( len > max_len )); then
                max_len=$len
                max_word=$j
            fi
        done
        echo "$i: '$max_word' ($max_len characters)"
    fi
done

Como podemos ver, hemos cambiado el bucle más externo para usar for en lugar de while. Al omitir la lista de palabras en el comando for, se usan los parámetros posicionales en su lugar. Dentro del bucle, las instancias previas de la variable i han sido cambiadas a la variable j. El uso de shift también ha sido eliminado.

miércoles, 14 de junio de 2017

Control de flujo: Bucles con for

En este último capítulo sobre control de flujo, veremos otra de las estructuras de bucles del shell. El bucle for difiere de los bucles while y until en que ofrece un medio de procesado de secuencias durante un bucle. Esto resulta muy útil cuando estamos programando. Por consiguiente, el bucle for es una estructura muy popular en scripting de bash.

Un bucle for se implementa, naturalmente, con el comando for. En versiones modernas de bash, for está disponible en dos formas.

lunes, 12 de junio de 2017

Para saber más

  • Además de las técnicas vistas en este capítulo, bash incluye un comando integrado llamado getopts, que también puede usarse para procesar argumentos de línea de comandos. Está descrito en la sección SHELL BUILTIN COMMAND de la man page de bash y en la Bash Hackers Wiki:
    http://wiki.bash-hackers.org/howto/getopts_tutorial

viernes, 9 de junio de 2017

Resumiendo

Con la inclusión de los parámetros posicionales, ahora podemos escribir scripts bastante funcionales. Para tareas simples y repetitivas, los parámetros posicionales hacen posible escribir funciones de shell muy útiles que pueden guardarse en el archivo .bashrc del usuario.

Nuestro programa sys_info_page ha crecido en complejidad y sofisticación. Aquí tenemos un listado completo , con los cambios más recientes resaltados:

#!/bin/bash

# sys_info_page: program to output a system information page

PROGNAME=$(basename $0)
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIMESTAMP="Generated $CURRENT_TIME, by $USER"

report_uptime () {
   cat <<- _EOF_
      <H2>System Uptime</H2>
      <PRE>$(uptime)</PRE>
      _EOF_
   return
}

report_disk_space () {
   cat <<- _EOF_
      <H2>Disk Space Utilization</H2>
      <PRE>$(df -h)</PRE>
      _EOF_
   return
}

report_home_space () {
   if [[ $(id -u) -eq 0 ]]; then
      cat <<- _EOF_
         <H2>Home Space Utilization (All Users)</H2>
         <PRE>$(du -sh /home/*)</PRE>
         _EOF_
   else
      cat <<- _EOF_
         <H2>Home Space Utilization ($USER)</H2>
      <PRE>$(du -sh $HOME)</PRE>
      _EOF_
   fi
   return
}

usage () {
   echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
   return
}

write_html_page () {
   cat <<- _EOF_
   <HTML>
      <HEAD>
         <TITLE>$TITLE</TITLE>
      </HEAD>
      <BODY>
         <H1>$TITLE</H1>
         <P>$TIMESTAMP</P>
         $(report_uptime)
         $(report_disk_space)
         $(report_home_space)
      </BODY>
   </HTML>
   _EOF_
   return
}

# process command line options

interactive=
filename=

while [[ -n $1 ]]; do
   case $1 in
      -f | --file)        shift
                          filename=$1
                          ;;
      -i | --interactive) interactive=1
                          ;;
      -h | --help)        usage
                          exit
                          ;;
      *)                  usage >&2
                          exit 1
                          ;;
   esac
   shift
done

# interactive mode

if [[ -n $interactive ]]; then
   while true; do
      read -p "Enter name of output file: " filename
      if [[ -e $filename ]]; then
        read -p "'$filename' exists. Overwrite? [y/n/q] > "
        case $REPLY in
            Y|y) break
                 ;;
            Q|q) echo "Program terminated."
                 exit
                 ;;
            *)   continue
                 ;;
        esac
      fi
   done
fi

# output html page

if [[ -n $filename ]]; then
   if touch $filename && [[ -f $filename ]]; then
      write_html_page > $filename
   else
      echo "$PROGNAME: Cannot write file '$filename'" >&2
      exit 1
   fi
else
   write_html_page
fi

Aún no hemos terminado. Todavía hay más cosas que podemos hacer y mejoras que podemos realizar.

miércoles, 7 de junio de 2017

Una aplicación más completa

Tras un largo paréntesis vamos a retomar nuestro trabajo con nuestro programa sys_info_page. Nuestra próxima mejora añadirá al programa varias opciones de línea de comandos de la siguiente manera:
  • Archivo de salida. Añadiremos una opción para especificar un nombre para un archivo que contenga la salida del programa. Se especificará como -f archivo o como --file archivo.
  • Modo interactivo. Esta opción preguntará al usuario un nombre para el archivo de salida y comprobará si el archivo especificado ya existe. Si es así, se le preguntará al usuario antes de que el archivo existente se sobrescriba. Esta opción se especificará como -i o como --interactive.
  • Ayuda. Tanto -h como --help pueden especificarse para hacer que el programa produzca un mensaje informativo de uso.
Aquí está el código necesario para implementar el procesado en la línea de comandos:

usage () {
    echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
    return
}

# process command line options

interactive=

filename=

while [[ -n $1 ]]; do
    case $1 in
        -f | --file)        shift
                            filename=$1
                            ;;
        -i | --interactive) interactive=1
                            ;;
        -h | --help)        usage
                            exit
                            ;;
        *)                  usage >&2
                            exit 1
                            ;;
    esac
    shift
done

Primero, añadimos una función de shell llamada usage para mostrar un mensaje cuando se invoca la opción de ayuda o se intenta una opción desconocida.

A continuación, comenzamos el bucle de procesamiento. Este bucle continúa mientras que el parámetro posicional $1 no esté vacío. Al final del bucle, tenemos un comando shift para avanzar los parámetros posicionales y asegurarnos de que el bucle finalmente terminará.

Dentro del bucle, tenemos una sentencia case que examina el parámetro posicional actual para ver si coincide con alguna de las opciones soportadas. Si encuentra un parámetro soportado, se actúa sobre él. Si no, se muestra el mensaje de uso y el script termina con un error.

El parámetro -f se maneja de una forma interesante. Cuando se detecta, hace que ocurra un shift adicional, que avanza el parámetro posicional $1 al argumento de nombre de archivo proporcionado por la opción -f.

A continuación añadimos el código para implementar el modo interactivo:

# interactive mode

if [[ -n $interactive ]]; then
   while true; do
      read -p "Enter name of output file: " filename
      if [[ -e $filename ]]; then
        read -p "'$filename' exists. Overwrite? [y/n/q] > "
        case $REPLY in
           Y|y) break
                ;;
           Q|q) echo "Program terminated."
                exit
                ;;
           *)   continue
                ;;
        esac
      elif [[ -z $filename ]]; then
         continue
      else
         break
      fi
   done
fi

Si la variable interactive no está vacía, se inicia un bucle infinito, que contiene el prompt del nombre del archivo y el consiguiente código existente de manejo de archivos. Si el archivo de salida deseado ya existe, se pregunta al usuario si quiere sobrescribir, elegir otro nombre de archivo o salir del programa. Si el usuario elige sobrescribir un archivo existente, se ejecuta un break para terminar el bucle. Fíjate cómo la sentencia case sólo detecta si el usuario sobrescribe o sale. Cualquier otra opción hace que el bucle continúe y pregunte al usuario de nuevo.

Para implementar la función de nombre de archivo de salida, primero deberíamos convertir el código de escritura de página existente en un función de shell, por las razones que veremos claras en un momento:

write_html_page () {
    cat <<- _EOF_
    <HTML>
        <HEAD>
            <TITLE>$TITLE</TITLE>
        </HEAD>
        <BODY>
            <H1>$TITLE</H1>
            <P>$TIMESTAMP</P>
            $(report_uptime)
            $(report_disk_space)
            $(report_home_space)
        </BODY>
    </HTML>
    _EOF_
    return
}

# output html page

if [[ -n $filename ]]; then
    if touch $filename && [[ -f $filename ]]; then
        write_html_page > $filename
    else
        echo "$PROGNAME: Cannot write file '$filename'" >&2
        exit 1
    fi
else
    write_html_page
fi

El código que maneja la lógica de la opción -f aparece al final de la lista mostrada anteriormente. En él, comprobamos la existencia de un nombre de archivo, y si encuentra uno, se realiza un test para ver si el archivo es modificable. Para hacer esto, se realiza un touch, seguido de un test para determinar si el archivo resultante es un archivo normal. Estos dos tests se encargan de situaciones donde se le indica una ruta no válida (touch fallará), y si el archivo ya existe, que sea un archivo normal.

Como podemos ver, se llama a la función write_html_page para realizar la generación de la página. Su salida es dirigida a la salida estándar (si la variable file-name está vacía) o redirigida a un archivo especificado.

lunes, 5 de junio de 2017

Manejando parámetros posicionales en masa

A veces es útil manejar todos los parámetros posicionales como un grupo. Por ejemplo, podríamos querer escribir un "envoltorio" alrededor de otro programa. Esto significa que creamos un script o función de shell que simplifique la ejecución de otro programa. El envoltorio proporciona una lista de opciones arcanas de la línea de comandos y luego pasa una lista de argumentos al programa de nivel inferior.

El shell ofrece dos parámetros especiales para este propósito. Ambos se expanden en la lista completa de parámetros posicionales, pero difieren en cosas muy sutiles. Son:

Tabla 32-1: Los parámetros especiales * y @
Parámetro Descripción
$* Se expande en la lista de parámetros posicionales, comenzando por 1. Cuando lo incluimos entre comillas dobles, se expande en una cadena con comillas dobles conteniendo todos los parámetros posicionales, cada uno separado por el primer carácter de la variable de shell IFS (por defecto un carácter espacio).
$@ Se expande en la lista de parámetros posicionales, comenzando por 1. Cuando lo incluimos entre comillas dobles, expande cada parámetro posicional en una palabra separada entre comillas dobles.

Aquí tenemos un script que muestra estos parámetros especiales en acción:

#!/bin/bash

# posit-params3 : script to demonstrate $* and $@

print_params () {
    echo "\$1 = $1"
    echo "\$2 = $2"
    echo "\$3 = $3"
    echo "\$4 = $4"
}

pass_params () {
    echo -e "\n" '$* :'; print_params $*
    echo -e "\n" '"$*" :'; print_params "$*"
    echo -e "\n" '$@ :'; print_params $@
    echo -e "\n" '"$@" :'; print_params "$@"
}

pass_params "word" "words with spaces"

En este programa tan complicado, creamos dos argumentos: "word" y "words with spaces", y los pasamos a la función pass_params. Esa función, a su vez, los pasa a la función print_params, usando cada uno de los cuatro métodos disponibles con los parámetros especiales $! y $@. Cuando lo ejecutamos, el script revela las diferencias:

[me@linuxbox ~]$ posit-param3

 $* :
$1 = word
$2 = words
$3 = with
$4 = spaces

 "$*" :
$1 = word words with spaces
$2 =
$3 =
$4 =

 $@ :
$1 = word
$2 = words
$3 = with
$4 = spaces

 "$@" :
$1 = word
$2 = words with spaces
$3 =
$4 =

Con nuestros argumentos, tanto $! como $@ producen un resultado de cuatro palabras:

word words with spaces

"$*" produce un resultado de una palabra:

    "word words with spaces"

"$@" produce un resultado de dos palabras:

    "word" "words with spaces"

que coincide con nuestra intención real. La lección que aprendemos de esto es que aunque el shell proporciona cuatro formas diferentes de obtener la lista de parámetros posicionales, "$@" es de lejos la más útil para la mayoría de los casos, porque conserva la integridad de cada parámetro posicional.

viernes, 2 de junio de 2017

Usando parámetros posicionales con funciones de shell

Además de usar los parámetros posicionales para pasar argumentos a scripts de shell, pueden usarse para pasar argumentos a funciones de shell. Para comprobarlo, convertiremos el script file_info en una función de shell:

file_info () {

    # file_info: function to display file information

    if [[ -e $1 ]]; then
        echo -e "\nFile Type:"
        file $1
        echo -e "\nFile Status:"
        stat $1
    else
        echo "$FUNCNAME: usage: $FUNCNAME file" >&2
        return 1
    fi
}

Ahora, si un script que incorpora la función de shell file_info llama a la función con un argumento de nombre de archivo, el argumento se pasará a la función.

Con esta capacidad, podemos escribir muchas funciones de shell útiles que no sólo pueden ser usadas con scripts, si no también dentro del archivo .bashrc.

Fíjate que la variable PROGNAME ha cambiado a la variable de shell FUNCNAME. El shell actualiza automáticamente esta variable para mantener el control de la función de shell ejecutada actualmente. Fíjate que $0 siempre contiene la ruta completa del primer elemento de la linea de comandos (es decir, el nombre del programa) y no contiene el nombre de la función de shell como podríamos esperar.