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" | > tr '[:upper:]' '[:lower:]' | > grep -oE "[a-z\']{2,}" | > sort | > uniq -c | > sort -nr | > head -n 10 1839 the 942 and 811 to 638 of 610 it 553 she 486 you 462 said 435 in 403 alice
Descarga un ebook utilizando
curl
.Convierte todo el texto a minúsculas utilizando
tr
.1Extrae todas las palabras utilizando
grep
2 y pon cada palabra en una línea distinta.Ordena estas palabras por orden alfabético utilizando
sort
.Elimina todos los duplicados y cuenta cuántas veces aparece cada palabra en la lista utilizando
uniq
.3Ordena esta lista de palabras únicas por su número en orden descendente utilizando
sort
.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 | > 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
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:
-
Copia y pega la frase en un archivo.
-
Añade permisos de ejecución.
-
Define el llamado shebang.
-
Retira la pieza de entrada fija.
-
Añade un parámetro.
-
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 bash
5 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, rwx
indican 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/bash
o !/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 python
7 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 python
porque el ejecutable env
8 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 de10
. -
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.