Capítulo 4. Creación de herramientas de línea de comandos

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

A lo largo del libro, te presentaré muchos comandos y combinaciones de comandos que básicamente caben en una sola línea. Son lo que se conoce como líneas de comandos o pipelines. Poder realizar tareas complejas con una sola línea de comandos es lo que hace poderosa a la línea de comandos. Es una experiencia muy diferente a escribir y utilizar programas tradicionales.

Algunas tareas las realizas una sola vez, y otras más a menudo. Algunas tareas son muy específicas, mientras que otras pueden generalizarse. Si necesitas repetir una determinada secuencia de comandos de forma regular, merece la pena convertirla en una herramienta de línea de comandos propia. Así que tanto las secuencias de comandos como las herramientas de línea de comandos tienen su utilidad. Reconocer la oportunidad de convertir una frase o un código existente en una herramienta de línea de comandos requiere práctica y habilidad. Las ventajas de una herramienta de línea de comandos son que no tienes que recordar toda la frase y que mejora la legibilidad si la incluyes en alguna otra cadena. En ese sentido, puedes pensar en una herramienta de línea de comandos como algo similar a una función en un lenguaje de programación.

Sin embargo, la ventaja de trabajar con un lenguaje de programación es que el código está en uno o varios archivos, lo que significa que puedes editarlo y reutilizarlo fácilmente. Si el código tiene parámetros, incluso puede generalizarse y volver a aplicarse a problemas que sigan un patrón similar.

Las herramientas de línea de comandos tienen lo mejor de ambos mundos: se pueden utilizar desde la línea de comandos, aceptan parámetros y sólo hay que crearlas una vez. En este capítulo, vas a familiarizarte con la creación de herramientas de línea de comandos de dos formas. En primer lugar, explico cómo convertir esos comandos de una sola línea en herramientas de línea de comandos reutilizables. Añadiendo parámetros a tus comandos, puedes aportar la misma flexibilidad que ofrece un lenguaje de programación. Posteriormente, demuestro cómo crear herramientas de línea de comandos reutilizables a partir de código escrito en un lenguaje de programación. Siguiendo la filosofía Unix, tu código puede combinarse con otras herramientas de línea de comandos, que pueden estar escritas en un lenguaje totalmente distinto. En este capítulo, me centraré en tres lenguajes de programación: Bash, Python y R.

Creo que la creación de herramientas reutilizables de línea de comandos te convierte a la larga en un científico de datos más eficiente y productivo. Poco a poco irás creando tu propia caja de herramientas de ciencia de datos, de la que podrás extraer las herramientas existentes y aplicarlas a los problemas que te hayas encontrado anteriormente.

Consejo

Para convertir un texto de una línea en un script de shell , voy a utilizar un poco de shell scripting. Este libro sólo muestra un pequeño subconjunto de conceptos de shell scripting, como variables, condicionales y bucles. Un curso completo de shell scripting podría llenar un libro por sí solo y, por tanto, está fuera del alcance de éste. Si quieres profundizar más en shell scripting, te recomiendo el libro Classic Shell Scripting de Arnold Robbins y Nelson H. F. Beebe (O'Reilly).

Visión general

En este capítulo aprenderás a:

  • Convierte guiones de una línea en guiones shell parametrizados

  • Convierte el código Python y R existente en herramientas de línea de comandos reutilizables

Este capítulo comienza con los siguientes archivos:

$ cd /data/ch04
 
$ l
total 32K
-rwxr--r-- 1 dst dst 400 Jun 29 14:27 fizzbuzz.py*
-rwxr--r-- 1 dst dst 391 Jun 29 14:27 fizzbuzz.R*
-rwxr--r-- 1 dst dst 182 Jun 29 14:27 stream.py*
-rwxr--r-- 1 dst dst 147 Jun 29 14:27 stream.R*
-rwxr--r-- 1 dst dst 105 Jun 29 14:27 top-words-4.sh*
-rwxr--r-- 1 dst dst 128 Jun 29 14:27 top-words-5.sh*
-rwxr--r-- 1 dst dst 647 Jun 29 14:27 top-words.py*
-rwxr--r-- 1 dst dst 584 Jun 29 14:27 top-words.R*

Las instrucciones para obtener estos archivos están en el Capítulo 2. Cualquier otro archivo se descarga o se genera utilizando herramientas de línea de comandos.

Convertir frases sencillas en scripts de shell

En esta sección voy a explicar cómo convertir una sola línea en una herramienta de línea de comandos reutilizable. Supongamos que quieres obtener las 10 palabras más utilizadas en un texto. Tomemos el libro Las aventuras de Alicia en el país de las maravillas, de Lewis Carroll, que, como muchos otros grandes libros, está disponible gratuitamente en el Proyecto Gutenberg:

$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | trim
The Project Gutenberg eBook of Alice’s Adventures in Wonderland, by Lewis...
 
This eBook is for the use of anyone anywhere in the United States and
most other parts of the world at no cost and with almost no restrictions
whatsoever. You may copy it, give it away or re-use it under the terms
of the Project Gutenberg License included with this eBook or online at
www.gutenberg.org. If you are not located in the United States, you
will have to check the laws of the country where you are located before
using this eBook.
 
... with 3751 more lines

La siguiente secuencia de herramientas, o pipeline, debería hacer el trabajo:

$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |  1
> tr '[:upper:]' '[:lower:]' |  2
> grep -oE "[a-z\']{2,}" |  3
> sort |  4
> uniq -c |  5
> sort -nr |  6
> head -n 10  7
   1839 the
    942 and
    811 to
    638 of
    610 it
    553 she
    486 you
    462 said
    435 in
    403 alice
1

Descarga un ebook utilizando curl.

2

Convierte todo el texto a minúsculas utilizando tr.1

3

Extrae todas las palabras utilizando grep2 y pon cada palabra en una línea distinta.

4

Ordena estas palabras por orden alfabético utilizando sort.

5

Elimina todos los duplicados y cuenta cuántas veces aparece cada palabra en la lista utilizando uniq.3

6

Ordena esta lista de palabras únicas por su número en orden descendente utilizando sort.

7

Conserva sólo las 10 primeras líneas (es decir, palabras) utilizando head.

Efectivamente, esas palabras son las que aparecen con más frecuencia en el texto. Como esas palabras (aparte de alice) aparecen con mucha frecuencia en muchos textos en inglés, tienen muy poco significado. De hecho, se conocen en como stopwords. Si nos deshacemos de ellas, nos quedamos con las palabras más frecuentes que están relacionadas con este texto.

Aquí tienes una lista de palabras clave que he encontrado:

$ curl -sL "https://raw.githubusercontent.com/stopwords-iso/stopwords-en/master/
stopwords-en.txt" |
> sort | tee stopwords | trim 20
10
39
a
able
ableabout
about
above
abroad
abst
accordance
according
accordingly
across
act
actually
ad
added
adj
adopted
ae
… with 1278 more lines

Con grep podemos filtrar las stopwords justo antes de empezar a contar:

$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
> tr '[:upper:]' '[:lower:]' |
> grep -oE "[a-z\']{2,}" |
> sort |
> grep -Fvwf stopwords |  1
> uniq -c |
> sort -nr |
> head -n 10
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
     63 king
     60 turtle
     57 mock
     56 hatter
     55 gryphon
1

Obtén los patrones de un archivo(stopwords en nuestro caso), un patrón por línea, con -f. Interpreta esos patrones como cadenas fijas con -F. Selecciona sólo las líneas que contengan coincidencias que formen palabras completas con -w. Selecciona las líneas sin coincidencias con-v.

Consejo

Cada herramienta de línea de comandos utilizada en este one-liner ofrece una página man. Así, en caso de que quieras saber más sobre, por ejemplo, grep, puedes ejecutar man grep desde la línea de comandos. Las herramientas de línea de comandos tr, grep, uniq y sort se tratarán con más detalle en el próximo capítulo.

No hay nada malo en ejecutar esta secuencia de comandos una sola vez. Sin embargo, imagina que quisieras tener las 10 palabras principales de cada libro electrónico en el Proyecto Gutenberg. O imagina que quisieras que las 10 palabras principales aparecieran en un sitio web de noticias cada hora. En esos casos, sería mejor tener esta secuencia de comandos como un bloque de construcción independiente que puede formar parte de algo más grande. Para añadir algo de flexibilidad a esta secuencia de comandos en cuanto a parámetros, convirtámosla en un script de shell.

Esto nos permite tomar la línea de comandos única como punto de partida y mejorarla gradualmente. Para convertir esta línea de comandos única en una herramienta de línea de comandos reutilizable, te guiaré a través de los seis pasos siguientes:

  1. Copia y pega la frase en un archivo.

  2. Añade permisos de ejecución.

  3. Define el llamado shebang.

  4. Retira la pieza de entrada fija.

  5. Añade un parámetro.

  6. Opcionalmente amplía tu PATH.

Paso 1: Crear un archivo

El primer paso es crear un nuevo archivo. Puedes abrir tu editor de texto favorito y copiar y pegar la línea de comandos. Vamos a llamar al archivo top-words-1.sh para indicar que éste es el primer paso hacia nuestra nueva herramienta de línea de comandos. Si te gusta quedarte en la línea de comandos, puedes utilizar el comando incorporado fc, que significa arreglar comando, y te permite arreglar o editar el último comando ejecutado:

$ fc

Al ejecutar fc se invoca el editor de texto predeterminado , que se almacena en la variable de entorno EDITOR. En el contenedor Docker, se establece en nano,4 un editor de texto sencillo. Como puedes ver, este archivo contiene nuestro one-liner:

  GNU nano 5.4                     /tmp/zsh9198lv                               
curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |                        
tr '[:upper:]' '[:lower:]' |            
grep -oE "[a-z\']{2,}" |                
sort |              
grep -Fvwf stopwords |                  
uniq -c |           
sort -nr |          
head -n 10          
 
 
 
 
                                [ Read 8 lines ]                                
^G Help      ^O Write Out ^W Where Is  ^K Cut       ^T Execute   ^C Location    
^X Exit      ^R Read File ^\ Replace   ^U Paste     ^J Justify   ^_ Go To Line  

Démosle a este archivo temporal un nombre propio pulsando Ctrl-O, eliminando el nombre de archivo temporal y escribiendo top-words-1.sh:

  GNU nano 5.4                     /tmp/zsh9198lv                               
curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |                        
tr '[:upper:]' '[:lower:]' |            
grep -oE "[a-z\']{2,}" |                
sort |              
grep -Fvwf stopwords |                  
uniq -c |           
sort -nr |          
head -n 10          
 
 
 
 
File Name to Write: top-words-1.sh                                              
^G Help             M-D DOS Format      M-A Append          M-B Backup File     
^C Cancel           M-M Mac Format      M-P Prepend         ^T Browse           

Pulsa Intro:

  GNU nano 5.4                     /tmp/zsh9198lv                               
curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |                        
tr '[:upper:]' '[:lower:]' |            
grep -oE "[a-z\']{2,}" |                
sort |              
grep -Fvwf stopwords |                  
uniq -c |           
sort -nr |          
head -n 10          
 
 
 
 
Save file under DIFFERENT NAME?                                                 
 Y Yes                                                                          
 N No           ^C Cancel                                                       

Pulsa Y para confirmar que quieres guardar con otro nombre de archivo:

  GNU nano 5.4                     top-words-1.sh                               
curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |                        
tr '[:upper:]' '[:lower:]' |            
grep -oE "[a-z\']{2,}" |                
sort |              
grep -Fvwf stopwords |                  
uniq -c |           
sort -nr |          
head -n 10          
 
 
 
 
                               [ Wrote 8 lines ]                                
^G Help      ^O Write Out ^W Where Is  ^K Cut       ^T Execute   ^C Location    
^X Exit      ^R Read File ^\ Replace   ^U Paste     ^J Justify   ^_ Go To Line  

Pulsa Ctrl-X para salir de nano y volver por donde has venido.

Estamos utilizando el archivo extensión .sh para dejar claro que estamos creando un script de shell. Sin embargo, las herramientas de línea de comandos no necesitan tener una extensión. De hecho, las herramientas de línea de comandos rara vez tienen extensiones.

Confirma el contenido del archivo:

$ pwd
/data/ch04
 
$ l
total 44K
-rwxr--r-- 1 dst dst  400 Jun 29 14:27 fizzbuzz.py*
-rwxr--r-- 1 dst dst  391 Jun 29 14:27 fizzbuzz.R*
-rw-r--r-- 1 dst dst 7.5K Jun 29 14:27 stopwords
-rwxr--r-- 1 dst dst  182 Jun 29 14:27 stream.py*
-rwxr--r-- 1 dst dst  147 Jun 29 14:27 stream.R*
-rw-r--r-- 1 dst dst  173 Jun 29 14:27 top-words-1.sh
-rwxr--r-- 1 dst dst  105 Jun 29 14:27 top-words-4.sh*
-rwxr--r-- 1 dst dst  128 Jun 29 14:27 top-words-5.sh*
-rwxr--r-- 1 dst dst  647 Jun 29 14:27 top-words.py*
-rwxr--r-- 1 dst dst  584 Jun 29 14:27 top-words.R*
 
$ bat top-words-1.sh
───────┬────────────────────────────────────────────────────────────────────────
       │ File: top-words-1.sh
───────┼────────────────────────────────────────────────────────────────────────
   1   │ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
   2   │ tr '[:upper:]' '[:lower:]' |
   3   │ grep -oE "[a-z\']{2,}" |
   4   │ sort |
   5   │ grep -Fvwf stopwords |
   6   │ uniq -c |
   7   │ sort -nr |
   8   │ head -n 10
───────┴────────────────────────────────────────────────────────────────────────

Ahora puedes utilizar bash5 para que interprete y ejecute los comandos del archivo:

$ bash top-words-1.sh
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
     63 king
     60 turtle
     57 mock
     56 hatter
     55 gryphon

Así te ahorras tener que volver a escribir la misma frase la próxima vez.

Sin embargo, como el archivo no puede ejecutarse por sí solo, todavía no es una verdadera herramienta de línea de comandos. Cambiemos eso en el siguiente paso.

Paso 2: Dar permiso para ejecutar

La razón por la que no podemos ejecutar directamente nuestro archivo es que no tenemos los permisos de acceso correctos. En concreto, tú, como usuario, necesitas tener permiso para ejecutar el archivo. En este apartado cambiamos los permisos de acceso de nuestro archivo.

Para comparar las diferencias entre los pasos, copia el archivo a top-words-2.sh utilizando cp -v top-words-{1,2}.sh.

Consejo

Si alguna vez quieres comprobar a qué conduce la expansión de llaves o cualquier otra forma de expansión de archivos, sustituye el comando por echo a e imprime el resultado, por ejemplo, echo book_{draft,final}.md o echo agent-{001..007}.

Para cambiar los permisos de acceso de un archivo, necesitamos utilizar una herramienta de línea de comandos llamada chmod,6 que significa cambiar modo. Cambia los bits de modo de un archivo concreto. El siguiente comando da al usuario (tú), permiso para ejecutar top-words-2.sh:

$ cp -v top-words-{1,2}.sh
'top-words-1.sh' -> 'top-words-2.sh'
 
$ chmod u+x top-words-2.sh

El argumento u+x consta de tres caracteres: (1) u indica que queremos cambiar los permisos del usuario propietario del archivo, que eres tú, porque tú creaste el archivo; (2) + indica que queremos añadir un permiso; y (3) x indica los permisos de ejecución.

Ahora echemos un vistazo a los permisos de acceso de ambos archivos:

$ l top-words-{1,2}.sh
-rw-r--r-- 1 dst dst 173 Jun 29 14:27 top-words-1.sh
-rwxr--r-- 1 dst dst 173 Jun 29 14:28 top-words-2.sh*

La primera columna muestra los permisos de acceso de cada archivo. Para top-words-2.sh, es -rwxr—r--El primer carácter, - (guión), indica el tipo de archivo. Un - significa archivo normal y d Los tres caracteres siguientes, rwxindican los permisos de acceso del usuario propietario del archivo. Los caracteres r y w significan lectura y escritura, respectivamente. (Como puedes ver, top-words-1.sh tiene un - en lugar de un x, lo que significa que no podemos ejecutar ese archivo). Los tres caracteres siguientes, rw-indican los permisos de acceso para todos los miembros del grupo al que pertenece el archivo. Finalmente, los tres últimos caracteres de la columna r--indican los permisos de acceso para todos los demás usuarios.

Ahora puedes ejecutar el archivo como se indica a continuación:

$ ./top-words-2.sh
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
     63 king
     60 turtle
     57 mock
     56 hatter
     55 gryphon

Si intentas ejecutar un archivo para el que no tienes los permisos de acceso correctos, como ocurre con top-words-1.sh, verás en el siguiente mensaje de error:

$ ./top-words-1.sh
zsh: permission denied: ./top-words-1.sh

Paso 3: Definir un Shebang

Aunque ya podemos ejecutar el archivo por sí solo, debemos añadirle el llamado shebang, que es una línea especial del script que indica al sistema qué ejecutable debe utilizar para interpretar los comandos.

El nombre shebang proviene de los dos primeros caracteres: una almohadilla (she) y un signo de exclamación (bang): #!. No es buena idea omitirlo como hemos hecho en el paso anterior, porque cada shell tiene un ejecutable por defecto diferente. El shell Z, que es el que estamos utilizando a lo largo del libro, utiliza el ejecutable /bin/sh por defecto si no se define shebang. En este caso me gustaría que bash interpretara los comandos , ya que eso nos dará algo más de funcionalidad de la que nos daría sh.

De nuevo, eres libre de utilizar el editor que quieras, pero yo me voy a quedar con nano, que está instalado en la imagen Docker:

$ cp -v top-words-{2,3}.sh
'top-words-2.sh' -> 'top-words-3.sh'
 
$ nano top-words-3.sh
  GNU nano 5.4                     top-words-3.sh                               
curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |                        
tr '[:upper:]' '[:lower:]' |            
grep -oE "[a-z\']{2,}" |                
sort |              
grep -Fvwf stopwords |                  
uniq -c |           
sort -nr |          
head -n 10          
 
 
 
 
                                [ Read 8 lines ]                                
^G Help      ^O Write Out ^W Where Is  ^K Cut       ^T Execute   ^C Location    
^X Exit      ^R Read File ^\ Replace   ^U Paste     ^J Justify   ^_ Go To Line  

Adelante, escribe #!/usr/bin/env bash y pulsa Intro. Cuando estés listo, pulsa Ctrl-X para guardar y salir:

  GNU nano 5.4                     top-words-3.sh *                             
#!/usr/bin/env bash 
curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |                        
tr '[:upper:]' '[:lower:]' |            
grep -oE "[a-z\']{2,}" |                
sort |              
grep -Fvwf stopwords |                  
uniq -c |           
sort -nr |          
head -n 10          
 
 
 
Save modified buffer?                                                           
 Y Yes                                                                          
 N No           ^C Cancel                                                       

Pulsa Y para indicar que quieres guardar el archivo:

  GNU nano 5.4                     top-words-3.sh *                             
#!/usr/bin/env bash 
curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |                        
tr '[:upper:]' '[:lower:]' |            
grep -oE "[a-z\']{2,}" |                
sort |              
grep -Fvwf stopwords |                  
uniq -c |           
sort -nr |          
head -n 10          
 
 
 
File Name to Write: top-words-3.sh                                              
^G Help             M-D DOS Format      M-A Append          M-B Backup File     
^C Cancel           M-M Mac Format      M-P Prepend         ^T Browse           

Confirmemos qué aspecto tiene top-words-3.sh:

$ bat top-words-3.sh
───────┬────────────────────────────────────────────────────────────────────────
       │ File: top-words-3.sh
───────┼────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env bash 
   2   │ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
   3   │ tr '[:upper:]' '[:lower:]' |
   4   │ grep -oE "[a-z\']{2,}" |
   5   │ sort |
   6   │ grep -Fvwf stopwords |
   7   │ uniq -c |
   8   │ sort -nr |
   9   │ head -n 10
───────┴────────────────────────────────────────────────────────────────────────

Eso es exactamente lo que necesitamos: nuestra tubería original con un tinglado delante.

A veces te encontrarás con scripts que tienen un tinglado en forma de !/usr/bin/basho !/usr/bin/python en el caso de Python (como veremos en la siguiente sección). Aunque esto suele funcionar, si los ejecutables de bash o python7 están instalados en una ubicación distinta de /usr/bin, entonces el script ya no funciona. Es mejor utilizar la forma que presento aquí, a saber !/usr/bin/env bash y !/usr/bin/env pythonporque el ejecutable env8 sabe dónde están instalados bash y python. En resumen, utilizar env hace que tus scripts sean más portables.

Paso 4: Elimina la Entrada Fija

Ahora tenemos una herramienta de línea de comandos válida que podemos ejecutar desde la línea de comandos. Pero podemos hacerlo aún mejor. Podemos hacer que nuestra herramienta de línea de comandos sea más reutilizable. El primer comando de nuestro archivo es curl, que descarga el texto del que deseamos obtener las 10 palabras más utilizadas. Así, los datos y las operaciones se combinan en uno.

¿Y si quisiéramos obtener las 10 palabras más utilizadas de otro libro electrónico, o de cualquier otro texto? Sería mejor separar los datos de entrada de la herramienta de línea de comandos.

Si suponemos que el usuario de la herramienta de línea de comandos proporcionará el texto, la herramienta será de aplicación general. Así que la solución es eliminar el comando curl del script. Aquí tienes el script actualizado llamado top-words-4.sh:

$ cp -v top-words-{3,4}.sh
'top-words-3.sh' -> 'top-words-4.sh'
 
$ sed -i '2d' top-words-4.sh
 
$ bat top-words-4.sh
───────┬────────────────────────────────────────────────────────────────────────
       │ File: top-words-4.sh
───────┼────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env bash 
   2   │ tr '[:upper:]' '[:lower:]' |
   3   │ grep -oE "[a-z\']{2,}" |
   4   │ sort |
   5   │ grep -Fvwf stopwords |
   6   │ uniq -c |
   7   │ sort -nr |
   8   │ head -n 10
───────┴────────────────────────────────────────────────────────────────────────

Esto funciona porque si un script empieza con un comando que necesita datos de la entrada estándar, como tr, tomará la entrada que se dé a las herramientas de la línea de comandos. Porejemplo:

$ curl -sL 'https://www.gutenberg.org/files/11/11-0.txt' | ./top-words-4.sh
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
     63 king
     60 turtle
     57 mock
     56 hatter
     55 gryphon
 
$ curl -sL 'https://www.gutenberg.org/files/12/12-0.txt' | ./top-words-4.sh
    469 alice
    189 queen
     98 gutenberg
     88 project
     72 time
     71 red
     70 white
     67 king
     63 head
     59 knight
 
$ man bash | ./top-words-4.sh
    585 command
    332 set
    313 word
    313 option
    304 file
    300 variable
    298 bash
    258 list
    257 expansion
    238 history
Consejo

Aunque no lo hemos hecho en nuestro script, el mismo principio es válido para guardar datos. En general, es mejor dejar que el usuario se encargue de ello utilizando la redirección de salida que dejar que el script escriba en un archivo específico. Por supuesto, si pretendes utilizar una herramienta de línea de comandos sólo para tus propios proyectos, entonces no hay límites a lo específico que puedes ser.

Paso 5: Añade argumentos

Hay un paso más para hacer que nuestra herramienta de línea de comandos sea aún más reutilizable: los parámetros. En nuestra herramienta de línea de comandos, , hay una serie de argumentos de línea de comandos fijos, por ejemplo, -nr para sort y -n 10 para head. Probablemente sea mejor mantener fijo el primer argumento. Sin embargo, sería muy útil permitir diferentes valores para el comando head. Esto permitiría al usuario final establecer el número de palabras más utilizadas que se mostrarán. A continuación se muestra el aspecto de nuestro archivo top-words-5.sh:

$ bat top-words-5.sh
───────┬────────────────────────────────────────────────────────────────────────
       │ File: top-words-5.sh
───────┼────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env bash
   2   │
   3   │ NUM_WORDS="${1:-10}"
   4   │
   5   │ tr '[:upper:]' '[:lower:]' |
   6   │ grep -oE "[a-z\']{2,}" |
   7   │ sort |
   8   │ grep -Fvwf stopwords |
   9   │ uniq -c |
  10   │ sort -nr |
  11   │ head -n "${NUM_WORDS}"
───────┴────────────────────────────────────────────────────────────────────────
  • La variable NUM_WORDS se establece con el valor de $1, que es una variable especial de Bash; contiene el valor del primer argumento de línea de comandos pasado a nuestra herramienta de línea de comandos. En la tabla siguiente se enumeran las demás variables especiales que ofrece Bash. Si no se especifica ningún valor, NUM_WORDS tomará el valor de 10.

  • Ten en cuenta que para utilizar el valor de la variable NUM_WORDS, tienes que ponerle un signo de dólar delante. Cuando la fijas, no escribes un signo de dólar.

Podríamos haber utilizado $1 directamente como argumento para head y no habernos molestado en crear una variable adicional como NUM_WORDS. Sin embargo, con guiones más grandes y algunos argumentos más en la línea de comandos, como $2 y $3, tu código resulta más legible cuando utilizas variables con nombre.

Ahora bien, si quisiéramos ver las 20 palabras más utilizadas de nuestro texto, invocaríamos nuestra herramienta de línea de comandos del siguiente modo:

$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" > alice.txt
 
$ < alice.txt ./top-words-5.sh 20
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
     63 king
     60 turtle
     57 mock
     56 hatter
     55 gryphon
     53 rabbit
     50 head
     48 voice
     45 looked
     44 mouse
     42 duchess
     40 tone
     40 dormouse
     37 cat
     34 march

Si el usuario no especifica un número, nuestro script mostrará las 10 palabras más comunes:

$ < alice.txt ./top-words-5.sh
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
     63 king
     60 turtle
     57 mock
     56 hatter
     55 gryphon

Paso 6: Amplía tu SENDERO

Después de los cinco pasos anteriores, por fin hemos terminado de construir una herramienta de línea de comandos reutilizable. Sin embargo, hay un paso más que puede ser muy útil. En este paso opcional, vamos a asegurarnos de que puedas ejecutar tus herramientas de línea de comandos desde cualquier lugar.

Actualmente, cuando quieres ejecutar tu herramienta de línea de comandos, tienes que navegar hasta el directorio en el que se encuentra o incluir la ruta de acceso completa, como se muestra en el paso 2. Esto está bien si la herramienta de línea de comandos se ha creado específicamente para un determinado proyecto. Sin embargo, si tu herramienta de línea de comandos puede aplicarse en múltiples situaciones, entonces es útil poder ejecutarla desde cualquier lugar, igual que las herramientas de línea de comandos que vienen con Ubuntu.

Para ello, Bash necesita saber dónde buscar tus herramientas de línea de comandos. Lo hace recorriendo una lista de directorios que se almacenan en una variable de entorno llamada PATH. En un contenedor Docker nuevo, PATH tiene este aspecto:

$ echo $PATH
/usr/local/lib/R/site-library/rush/exec:/usr/bin/dsutils:/home/dst/.local/bin:/u
sr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Los directorios están delimitados por dos puntos. Podemos imprimir este como una lista de directorios convirtiendo los dos puntos en nuevas líneas:

$ echo $PATH | tr ':' '\n'
/usr/local/lib/R/site-library/rush/exec
/usr/bin/dsutils
/home/dst/.local/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin

Para cambiar el PATH de forma permanente, tendrás que editar el archivo .bashrc o .profile que se encuentra en tu directorio personal. Si colocas todas tus herramientas personalizadas de línea de comandos en un directorio -digamos, ~/tools-,sólo tendrás que cambiar el PATH una vez. Ahora ya no tendrás que añadir el ./ y podrás utilizar simplemente el nombre del archivo. Además, ya no tendrás que recordar dónde se encuentra la herramienta de línea de comandos :

$ cp -v top-words{-5.sh,}
'top-words-5.sh' -> 'top-words'
 
$ export PATH="${PATH}:/data/ch04"
 
$ echo $PATH
/usr/local/lib/R/site-library/rush/exec:/usr/bin/dsutils:/home/dst/.local/bin:/u
sr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/data/ch04
 
$ curl "https://www.gutenberg.org/files/11/11-0.txt" |
> top-words 10
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  170k  100  170k    0     0   223k      0 --:--:-- --:--:-- --:--:--  223k
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
     63 king
     60 turtle
     57 mock
     56 hatter
     55 gryphon

Crear herramientas de línea de comandos con Python y R

La herramienta de línea de comandos que creamos en la sección anterior estaba escrita en Bash. (Claro que no se emplearon todas las características del lenguaje de programación Bash, pero el intérprete seguía siendo bash.) Como ya sabrás a estas alturas, la línea de comandos es agnóstica al lenguaje, así que no tenemos por qué utilizar Bash para crear herramientas de línea de comandos.

En esta sección voy a demostrar que las herramientas de línea de comandos también se pueden crear en otros lenguajes de programación. Me centraré en Python y R porque son los dos lenguajes de programación más populares dentro de la comunidad de la ciencia de datos. No puedo ofrecer una introducción completa a ninguno de los dos lenguajes, así que asumo que tienes cierta familiaridad con Python y/o R. Otros lenguajes de programación como Java, Go y Julia siguen un patrón similar cuando se trata de crear herramientas de línea de comandos.

Hay tres razones principales para crear herramientas de línea de comandos en un lenguaje de programación distinto de Bash. En primer lugar, puede que ya tengas algún código que te gustaría poder utilizar desde la línea de comandos. En segundo lugar, la herramienta de línea de comandos acabaría abarcando más de cien líneas de código Bash. En tercer lugar, la herramienta de línea de comandos tiene que ser más segura y robusta (Bash carece de muchas funciones, como la comprobación de tipos).

Los seis pasos que he comentado en la sección anterior se aplican también, a grandes rasgos, a la creación de herramientas de línea de comandos en otros lenguajes de programación. Sin embargo, el primer paso no sería copiar y pegar desde la línea de comandos, sino copiar y pegar el código pertinente en un archivo nuevo. Las herramientas de línea de comandos escritas en Python y R deben especificar python y Rscript,9 respectivamente, como intérprete después delshebang.

Cuando se trata de crear herramientas de línea de comandos utilizando Python y R, hay dos aspectos más que merecen especial atención. En primer lugar, el procesamiento de la entrada estándar, que es algo natural en los scripts de shell, tiene que tratarse explícitamente en Python y R. En segundo lugar, como las herramientas de línea de comandos escritas en Python y R tienden a ser más complejas, es posible que también queramos ofrecer al usuario la posibilidad de especificar argumentos de línea de comandos más elaborados.

Portar el Script Shell

Como punto de partida, veamos cómo podríamos portar el script de shell que acabamos de crear tanto a Python como a R. En otras palabras, ¿qué código de Python y de R nos da las palabras más utilizadas a partir de la entrada estándar? Primero mostraremos los dos archivos top-words.py y top-words.R y luego discutiremos las diferencias con el código shell. En Python, el código sería algo parecido a lo siguiente:

$ cd /data/ch04
 
$ bat top-words.py
───────┬────────────────────────────────────────────────────────────────────────
       │ File: top-words.py
───────┼────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env python
   2   │ import re
   3   │ import sys
   4   │
   5   │ from collections import Counter
   6   │ from urllib.request import urlopen
   7   │
   8   │ def top_words(text, n):
   9   │     with urlopen("https://raw.githubusercontent.com/stopwords-iso/stopw
ords-en/master/stopwords-en.txt") as f:
  10   │         stopwords = f.read().decode("utf-8").split("\n")
  11   │
  12   │     words = re.findall("[a-z']{2,}", text.lower())
  13   │     words = (w for w in words if w not in stopwords)
  14   │
  15   │     for word, count in Counter(words).most_common(n):
  16   │         print(f"{count:>7} {word}")
  17   │
  18   │
  19   │ if __name__ == "__main__":
  20   │     text = sys.stdin.read()
  21   │
  22   │     try:
  23   │         n = int(sys.argv[1])
  24   │     except:
  25   │         n = 10
  26   │
  27   │     top_words(text, n)
───────┴────────────────────────────────────────────────────────────────────────

Ten en cuenta que este ejemplo de Python no utiliza ningún paquete de terceros. Si quieres hacer procesamiento avanzado de texto, entonces te recomiendo que eches un vistazo al paquete NLTK.10 Si vas a trabajar con muchos datos numéricos, entonces te recomiendo que utilices el paquete Pandas.11

En R el código sería algo parecido a esto:

$ bat top-words.R
───────┬────────────────────────────────────────────────────────────────────────
       │ File: top-words.R
───────┼────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env Rscript
   2   │ n <- as.integer(commandArgs(trailingOnly = TRUE))
   3   │ if (length(n) == 0) n <- 10
   4   │
   5   │ f_stopwords <- url("https://raw.githubusercontent.com/stopwords-iso/sto
pwords-en/master/stopwords-en.txt")
   6   │ stopwords <- readLines(f_stopwords, warn = FALSE)
   7   │ close(f_stopwords)
   8   │
   9   │ f_text <- file("stdin")
  10   │ lines <- tolower(readLines(f_text))
  11   │
  12   │ words <- unlist(regmatches(lines, gregexpr("[a-z']{2,}", lines)))
  13   │ words <- words[is.na(match(words, stopwords))]
  14   │
  15   │ counts <- sort(table(words), decreasing = TRUE)
  16   │ cat(sprintf("%7d %s\n", counts[1:n], names(counts[1:n])), sep = "")
  17   │ close(f_text)
───────┴────────────────────────────────────────────────────────────────────────

Comprobemos que las tres implementaciones (es decir, Bash, Python y R) devuelven las mismas cinco palabras principales con los mismos recuentos:

$ time < alice.txt top-words 5
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
top-words 5 < alice.txt  0.08s user 0.01s system 107% cpu 0.084 total
 
$ time < alice.txt top-words.py 5
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
top-words.py 5 < alice.txt  0.38s user 0.02s system 82% cpu 0.478 total
 
$ time < alice.txt top-words.R 5
    403 alice
     98 gutenberg
     88 project
     76 queen
     71 time
top-words.R 5 < alice.txt  0.29s user 0.07s system 56% cpu 0.652 total

¡Maravilloso! Claro, el resultado en sí no es muy emocionante. Lo emocionante es que podemos realizar la misma tarea con varias lenguas. Veamos las diferencias entre los enfoques.

En primer lugar, lo que salta inmediatamente a la vista son las diferencias en la cantidad de código. Para esta tarea concreta, tanto Python como R requieren mucho más código que Bash. Esto ilustra que, para algunas tareas, es mejor utilizar la línea de comandos. Para otras tareas, puede que sea mejor utilizar un lenguaje de programación. A medida que adquieras más experiencia en la línea de comandos, empezarás a reconocer cuándo utilizar un enfoque u otro. Cuando todo sea una herramienta de línea de comandos, puedes incluso dividir la tarea en subtareas y combinar una herramienta de línea de comandos Bash con, por ejemplo, una herramienta de línea de comandos Python: el enfoque que mejor funcione para la tarea en cuestión.

Procesamiento de datos de entrada estándar

En los dos fragmentos de código anteriores de , tanto Python como R leen la entrada estándar completa de una vez. En la línea de comandos, la mayoría de las herramientas canalizan los datos a la siguiente herramienta de línea de comandos de forma continua. Algunas herramientas de línea de comandos, como sort, necesitan los datos completos antes de escribirlos en la salida estándar, lo que significa que estas herramientas bloquean la canalización. Esto no tiene por qué ser un problema cuando los datos de entrada son finitos, como un archivo. Sin embargo, cuando los datos de entrada son un flujo incesante, estas herramientas de línea de comandos que bloquean son inútiles.

Por suerte, Python y R admiten el procesamiento de datos en flujo. Puedes aplicar una función línea por línea, por ejemplo. Aquí tienes dos ejemplos mínimos que demuestran cómo funciona esto en Python y R, respectivamente.

Tanto las herramientas de Python como las de R resuelven el ya famoso problema Fizz Buzz, que se define así: imprime todos los números del 1 al 100, pero si el número es divisible por 3, imprime "fizz" en su lugar; si el número es divisible por 5, imprime "buzz"; y si el número es divisible por 15, imprime "fizzbuzz". Aquí tienes el código Python:12

$ bat fizzbuzz.py
───────┬────────────────────────────────────────────────────────────────────────
       │ File: fizzbuzz.py
───────┼────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env python
   2   │ import sys
   3   │
   4   │ CYCLE_OF_15 = ["fizzbuzz", None, None, "fizz", None,
   5   │                "buzz", "fizz", None, None, "fizz",
   6   │                "buzz", None, "fizz", None, None]
   7   │
   8   │ def fizz_buzz(n: int) -> str:
   9   │     return CYCLE_OF_15[n % 15] or str(n)
  10   │
  11   │ if __name__ == "__main__":
  12   │     try:
  13   │         while (n:= sys.stdin.readline()):
  14   │             print(fizz_buzz(int(n)))
  15   │     except:
  16   │         pass
───────┴────────────────────────────────────────────────────────────────────────

Y aquí está el código R:

$ bat fizzbuzz.R
───────┬────────────────────────────────────────────────────────────────────────
       │ File: fizzbuzz.R
───────┼────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env Rscript
   2   │ cycle_of_15 <- c("fizzbuzz", NA, NA, "fizz", NA,
   3   │                  "buzz", "fizz", NA, NA, "fizz",
   4   │                  "buzz", NA, "fizz", NA, NA)
   5   │
   6   │ fizz_buzz <- function(n) {
   7   │   word <- cycle_of_15[as.integer(n) %% 15 + 1]
   8   │   ifelse(is.na(word), n, word)
   9   │ }
  10   │
  11   │ f <- file("stdin")
  12   │ open(f)
  13   │ while(length(n <- readLines(f, n = 1)) > 0) {
  14   │   write(fizz_buzz(n), stdout())
  15   │ }
  16   │ close(f)
───────┴────────────────────────────────────────────────────────────────────────

Vamos a probar ambas herramientas (para ahorrar espacio, he pasado la salida a column):

$ seq 30 | fizzbuzz.py | column -x
1               2               fizz            4               buzz
fizz            7               8               fizz            buzz
11              fizz            13              14              fizzbuzz
16              17              fizz            19              buzz
fizz            22              23              fizz            buzz
26              fizz            28              29              fizzbuzz
 
$ seq 30 | fizzbuzz.R | column -x
1               2               fizz            4               buzz
fizz            7               8               fizz            buzz
11              fizz            13              14              fizzbuzz
16              17              fizz            19              buzz
fizz            22              23              fizz            buzz
26              fizz            28              29              fizzbuzz

Esta salida me parece correcta! Es difícil demostrar que estas dos herramientas funcionan realmente de forma fluida. Puedes verificarlo tú mismo pasando los datos de entrada a sample -d 100 antes de pasarlos a la herramienta Python o R. De esta forma, añadirás un pequeño retraso entre cada línea para que sea más fácil confirmar que las herramientas no esperan a todos los datos de entrada de , sino que operan línea a línea.

Resumen

En este capítulo intermedio, te he mostrado cómo crear tu propia herramienta de línea de comandos. Sólo son necesarios seis pasos para convertir tu código en un bloque de construcción reutilizable, que verás que te hace mucho más productivo. Te aconsejo que estés atento a las oportunidades de crear tus propias herramientas. El siguiente capítulo trata del segundo paso del modelo OSEMN para la ciencia de datos, a saber, la depuración de datos.

Para seguir explorando

  • Añadir documentación de ayuda a tu herramienta se convierte en algo importante cuando la herramienta tiene muchas opciones que recordar, y más aún cuando quieres compartir tu herramienta con otros. docopt es un marco de trabajo independiente del lenguaje para proporcionar ayuda y definir las opciones que acepta tu herramienta. Hay implementaciones disponibles en casi cualquier lenguaje de programación, incluidos Bash, Python y R.

  • Si quieres aprender más sobre programación en Bash, te recomiendo Classic Shell Programming de Arnold Robbins y Nelson H. F. Beebe (O'Reilly) y Bash Cookbook, 2ª Edición de Carl Albing y JP Vossen (O'Reilly).

  • Escribir un script Bash robusto y seguro es bastante complicado. ShellCheck es una herramienta en línea que comprobará tu código Bash en busca de errores y vulnerabilidades. También hay disponible una herramienta de línea de comandos.

  • El libro Ten Essays on Fizz B uzz de Joel Grus (Brightwalton) es una colección perspicaz y divertida de 10 formas diferentes de resolver Fizz Buzz con Python.

1 Jim Meyering, tr - Traducir o eliminar caracteres, versión 8.30, 2018, https://www.gnu.org/software/coreutils.

2 Jim Meyering, grep - Imprimir líneas que coincidan con patrones, versión 3.4, 2019, https://www.gnu.org/software/grep.

3 Richard M. Stallman y David MacKenzie, uniq - Informar u omitir líneas repetidas, versión 8.30, 2019, https://www.gnu.org/software/coreutils.

4 Benno Schulenberg et al., nano - Editor ANOther de Nano, inspirado en Pico, versión 5.4, 2020, https://nano-editor.org.

5 Brian Fox y Chet Ramey, bash - GNU Bourne-Again SHell, versión 5.0.17, 2019, https://www.gnu.org/software/bash.

6 David MacKenzie y Jim Meyering, chmod - Cambiar bits de modo de archivo, versión 8.30, 2018, https://www.gnu.org/software/coreutils.

7 The Python Software Foundation, python - an Interpreted, Interactive, Object-Oriented Programming Language, versión 3.8.5, 2021, https://www.python.org.

8 Richard Mlynarik, David MacKenzie y Assaf Gordon, env - Ejecutar un programa en un entorno modificado, versión 8.32, 2020, https://www.gnu.org/software/coreutils.

9 Fundación R para la Computación Estadística, R - a Language and Environment for Statistical Computing, versión 4.0.4, 2021, https://www.r-project.org.

10 Jacob Perkins, Python Text Processing with NLTK 2.0 Cookbook (Birmingham, Reino Unido: Packt, 2010).

11 Wes McKinney, Python para el análisis de datos (O'Reilly, 2017).

12 Este código está adaptado de un script en Python de Joel Grus.

Get Ciencia de datos en la línea de comandos, 2ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.