Trabajando con matrices en bash

Los programadores usan bash regularmente para resolver muchas tareas relacionadas con el desarrollo de software. Al mismo tiempo, las matrices bash a menudo se consideran una de las características más incomprensibles de este shell (probablemente, las matrices solo son superadas por las expresiones regulares a este respecto). El autor del material, cuya traducción publicamos hoy, invita a todos al maravilloso mundo de las matrices de bash que, si se acostumbra a su sintaxis inusual, puede traer muchos beneficios.

imagen

El verdadero desafío de que las matrices bash sean útiles


Escribir sobre bash es controvertido. El hecho es que los artículos sobre bash a menudo se convierten en guías de usuario dedicadas a historias sobre las características sintácticas de los comandos en cuestión. Este artículo está escrito de manera diferente, esperamos que no lo encuentre en el próximo "manual de usuario".

Dado lo anterior, imagine un escenario real para usar matrices en bash. Suponga que se enfrenta a la tarea de evaluar y optimizar una utilidad a partir de un nuevo conjunto interno de herramientas utilizadas en su empresa. En el primer paso de este estudio, debe probarlo con diferentes conjuntos de parámetros. La prueba tiene como objetivo estudiar cómo se comporta un nuevo conjunto de herramientas cuando utilizan un número diferente de subprocesos. Para simplificar la presentación, suponemos que la "caja de herramientas" es una "caja negra" compilada a partir de código C ++. Al usarlo, el único parámetro en el que podemos influir es el número de subprocesos reservados para el procesamiento de datos. Llamar al sistema bajo investigación desde la línea de comando se ve así:

./pipeline --threads 4 

Los fundamentos


En primer lugar, declaramos una matriz que contiene los valores del parámetro --threads con los que queremos probar el sistema. Esta matriz se ve así:

 allThreads=(1 2 4 8 16 32 64 128) 

En este ejemplo, todos los elementos son números, pero de hecho, en las matrices bash, puede almacenar tanto números como cadenas al mismo tiempo. Por ejemplo, la declaración de dicha matriz es bastante aceptable:

 myArray=(1 2 "three" 4 "five") 

Al igual que con otras variables bash, asegúrese de que no haya espacios alrededor del signo = . De lo contrario, bash considerará el nombre de la variable como el nombre del programa que necesita ejecutar, ¡y = su primer argumento!

Ahora que hemos inicializado la matriz, extraigamos algunos elementos de ella. Aquí puede observar, por ejemplo, que el echo $allThreads solo generará el primer elemento de la matriz.

Para comprender las razones de este comportamiento, vamos a desviarnos un poco de las matrices y recordar cómo trabajar con variables en bash. Considere el siguiente ejemplo:

 type="article" echo "Found 42 $type" 

Suponga que tiene una variable $type que contiene una cadena que representa un sustantivo. Después de esta palabra, agregue la letra s . Sin embargo, no puede simplemente agregar esta letra al final del nombre de la variable, ya que esto convertirá el comando para acceder a la variable en $types , es decir, trabajaremos con una variable completamente diferente. En esta situación, puede usar una construcción como echo "Found 42 "$type"s" . Pero es mejor resolver este problema usando llaves: echo "Found 42 ${type}s" , que nos permite decirle a bash dónde comienza y termina el nombre de la variable (curiosamente, la misma sintaxis se usa en JavaScript ES6 para incorporar variables en expresiones en cadenas de patrones ).

Ahora volvamos a las matrices. Resulta que, aunque las llaves no son necesarias cuando se trabaja con variables, son necesarias para trabajar con matrices. Le permiten establecer índices para acceder a los elementos de la matriz. Por ejemplo, un comando de la forma echo ${allThreads[1]} generará el segundo elemento de la matriz. Si olvida las llaves en la construcción anterior, bash percibirá [1] como una cadena y procesará lo que suceda en consecuencia.

Como puede ver, las matrices en bash tienen una sintaxis extraña, pero en ellas, al menos, la numeración de los elementos comienza desde cero. Esto los hace similares a las matrices de muchos otros lenguajes de programación.

Formas de acceder a los elementos de la matriz


En el ejemplo anterior, utilizamos índices enteros en matrices que se especifican explícitamente. Ahora considere dos formas más de trabajar con matrices.

El primer método es aplicable si necesitamos el elemento $i ésimo de la matriz, donde $i es una variable que contiene el índice del elemento de matriz deseado. Puede extraer este elemento de la matriz utilizando una construcción de la forma echo ${allThreads[$i]} .

El segundo método le permite mostrar todos los elementos de la matriz. Consiste en reemplazar el índice numérico con el símbolo @ (se puede interpretar como un comando que apunta a todos los elementos de la matriz). Se ve así: echo ${allThreads[@]} .

Iterando sobre elementos de matriz en bucles


Los principios anteriores de trabajar con elementos de matriz nos serán útiles para resolver el problema de enumerar elementos de matriz. En nuestro caso, esto significa lanzar el comando de pipeline en estudio con cada uno de los valores, que simboliza el número de subprocesos y se almacena en una matriz. Se ve así:

 for t in ${allThreads[@]}; do ./pipeline --threads $t done 

Enumeración de índices de matriz en bucles


Ahora considere un enfoque ligeramente diferente para ordenar las matrices. En lugar de iterar sobre los elementos, podemos iterar sobre los índices de la matriz:

 for i in ${!allThreads[@]}; do ./pipeline --threads ${allThreads[$i]} done 

Analicemos lo que está sucediendo aquí. Como ya hemos visto, una construcción de la forma ${allThreads[@]} representa todos los elementos de la matriz. Cuando agregamos un signo de exclamación aquí, convertimos esta construcción en ${!allThreads[@]} , lo que lleva al hecho de que devuelve los índices de la matriz (de 0 a 7 en nuestro caso).

En otras palabras, el ciclo for sobre todos los índices de la matriz representada como la variable $i , y en el cuerpo del ciclo, se --thread los elementos de la matriz que sirven como valores del parámetro ${allThreads[$i]} usando la ${allThreads[$i]} .

Leer este código es más difícil que el del ejemplo anterior. Por lo tanto, surge la pregunta de para qué sirven todas estas dificultades. Y necesitamos esto porque en algunas situaciones, cuando se procesan matrices en bucles, debe conocer tanto los índices como los valores de los elementos. Por ejemplo, si necesita omitir el primer elemento de una matriz, iterar sobre los índices nos salvará, por ejemplo, de la necesidad de crear una variable adicional y de incrementarla en un bucle para trabajar con elementos de la matriz.

Relleno de matrices


Hasta ahora, hemos explorado el sistema invocando el comando de pipeline y pasándole cada uno de los valores del parámetro --threads nos interesa. Ahora suponga que este comando proporciona la duración de un determinado proceso en segundos. Nos gustaría interceptar los datos que se le devuelven en cada iteración y guardarlos en otra matriz. Esto nos dará la oportunidad de trabajar con los datos almacenados una vez que hayan finalizado todas las pruebas.

Construcciones de sintaxis útiles


Antes de hablar sobre cómo agregar datos a las matrices, veamos algunas construcciones de sintaxis útiles. Para empezar, necesitamos un mecanismo para obtener la salida de datos mediante comandos bash. Para capturar la salida de un comando, debe usar la siguiente construcción:

 output=$( ./my_script.sh ) 

Después de ejecutar este comando, lo que myscript.sh se almacenará en la variable $output .

La segunda construcción, que será útil muy pronto, nos permite adjuntar nuevos datos a las matrices. Se ve así:

 myArray+=( "newElement1" "newElement2" ) 

Resolución de problemas


Ahora, si reúne todo lo que acabamos de aprender, puede crear un script para probar el sistema, que ejecuta un comando con cada uno de los valores de los parámetros de la matriz y almacena en la otra matriz lo que muestra este comando.

 allThreads=(1 2 4 8 16 32 64 128) allRuntimes=() for t in ${allThreads[@]}; do runtime=$(./pipeline --threads $t) allRuntimes+=( $runtime ) done 

Que sigue


Acabamos de examinar cómo usar las matrices bash para iterar sobre los parámetros utilizados al iniciar un programa y guardar los datos que devuelve este programa. Sin embargo, las opciones para usar matrices no se limitan a este escenario. Aquí hay un par de ejemplos más.

Alertas de problemas


En este escenario, veremos una aplicación que se divide en módulos. Cada uno de estos módulos tiene su propio archivo de registro. Podemos escribir un script de trabajo cron que, si se encuentran problemas en el archivo de registro correspondiente, notificará por correo electrónico a la persona responsable de cada uno de los módulos:

 #  -    logPaths=("api.log" "auth.log" "jenkins.log" "data.log") logEmails=("jay@email" "emma@email" "jon@email" "sophia@email") #         for i in ${!logPaths[@]}; do log=${logPaths[$i]} stakeholder=${logEmails[$i]} numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l ) #       5  if [[ "$numErrors" -gt 5 ]]; then   emailRecipient="$stakeholder"   emailSubject="WARNING: ${log} showing unusual levels of errors"   emailBody="${numErrors} errors found in log ${log}"   echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient" fi done 

Solicitudes API


Suponga que desea recopilar información sobre qué usuarios comentan sus publicaciones en Medium. Como no tenemos acceso directo a la base de datos de este sitio, no discutiremos las consultas SQL. Sin embargo, puede usar varias API para acceder a este tipo de datos.

Para evitar largas conversaciones sobre autenticación y tokens, utilizaremos, como punto final, el servicio público de prueba de API JSONPlaceholder . Después de recibir una publicación del servicio y extraer datos de su código en las direcciones de correo electrónico de los comentaristas, podemos poner estos datos en una matriz:

 endpoint="https://jsonplaceholder.typicode.com/comments" allEmails=() #   10  for postId in {1..10}; do #    API       response=$(curl "${endpoint}?postId=${postId}") #  jq   JSON       allEmails+=( $( jq '.[].email' <<< "$response" ) ) done 

Tenga en cuenta que la herramienta jq se usa aquí, lo que permite analizar JSON en la línea de comandos. No vamos a entrar en detalles sobre cómo trabajar con jq si está interesado en esta herramienta; consulte la documentación correspondiente.

Bash o Python?


Matrices: una función útil y está disponible no solo en bash. El que escribe scripts para la línea de comando puede tener una pregunta lógica sobre en qué situaciones vale la pena usar bash y en qué, por ejemplo, Python.

En mi opinión, la respuesta a esta pregunta radica en cuánto depende el programador de una tecnología en particular. Digamos, si el problema se puede resolver directamente en la línea de comando, entonces nada impide el uso de bash. Sin embargo, en el caso de que, por ejemplo, el script que le interesa sea parte de un proyecto escrito en Python, puede usar Python.

Por ejemplo, para resolver el problema considerado aquí, puede usar un script escrito en Python, sin embargo, esto se reducirá a escribir envoltorios para Python para bash:

 import subprocess all_threads = [1, 2, 4, 8, 16, 32, 64, 128] all_runtimes = [] #         for t in all_threads: cmd = './pipeline --threads {}'.format(t) #   subprocess   ,    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) output = p.communicate()[0] all_runtimes.append(output) 

Quizás la solución a este problema con bash, sin involucrar otras tecnologías, es más corta y más comprensible, y aquí puede prescindir por completo de Python.

Resumen


En este material analizamos muchos diseños utilizados para trabajar con matrices. Aquí hay una tabla donde encontrará lo que hemos revisado y algo nuevo.
Construcción de sintaxisDescripción
arr=()Crea una matriz vacía
arr=(1 2 3)Inicialización de matriz
${arr[2]}Obtener el tercer elemento de una matriz
${arr[@]}Obtener todos los elementos de la matriz
${!arr[@]}Obtener índices de matriz
${#arr[@]}Cálculo del tamaño de matriz
arr[0]=3Sobrescribir el primer elemento de una matriz
arr+=(4)Unirse a una matriz de valores
str=$(ls)Guardar la salida del ls como una cadena
arr=( $(ls) )Guardar la salida del ls como una matriz de nombres de archivo
${arr[@]:s:n}Obtener elementos de matriz de elemento con índice s a elemento con índice s+(n-1)

A primera vista, las matrices bash pueden parecer bastante extrañas, pero las posibilidades que ofrecen merecen la pena para hacer frente a estas rarezas. Creemos que habiendo dominado las matrices de bash, las usará con bastante frecuencia. Es fácil imaginar innumerables escenarios en los que estas matrices pueden ser útiles.

Estimados lectores! Si tiene ejemplos interesantes del uso de matrices en scripts de bash, compártalos.

Source: https://habr.com/ru/post/es413725/


All Articles