web-dev-qa-db-fra.com

Opérations mathématiques sur les tableaux à partir des lignes journalisées

tout à fait le débutant bash ici. J'essaie de connecter des lignes imprimées dans un tableau via bash. Je voudrais effectuer des opérations mathématiques (c'est-à-dire additionner des éléments au même emplacement) sur certains éléments d'un tel tableau et finalement renvoyer le tableau pour une utilisation ultérieure en dehors de la fonction.

Voici ce que je tripote avec:

linesToArraySum() {
while read line
do
    logLine=$line # saves currently logged line in variable logLine
    IFS=';' read -a arrayLog <<< $logLine #redirect variable logLine as input for read command. read -a saves Word of input string as array. InternalFieldSeparator set as ';' detects elements in input string which are separated by '; ' as words.
    for n in 1 3 5 7 9 11
    do
        arraySum[n]=$((${arraySum[n]} + ${arrayLog[n]})) # define element in arraySum at position n as sum of previous element and element in arrayLog at this position
        echo ${arraySum[n]}
    done
    return arraySum
done
}

Comme mentionné ci-dessus, les lignes enregistrées sont imprimées en continu via ttylog, mais pour le dépannage, supposons que je les génère avec le script suivant:

while [[ $i < 9 ]] 
do 
    i=$(($i + 1))
    echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935" | linesToArraySum
done
commandDoSomethingWith_arraySum

Mon le problème est que echo $ {arraySum [n]} dans la fonction linesToArraySum () renvoie toujours la valeur actuelle de $ {array [n]} au lieu de la somme de valeurs identiques colonne.

J'apprécierais beaucoup toute suggestion concernant mes erreurs.\o /

2
brunuser

Dans Bash, chaque commande d'un pipline (|) s'exécute dans un sous-shell .Les modifications apportées aux variables à l'intérieur d'un sous-shell, y compris les affectations apportées aux éléments de tableau, ne sont pas répercutées dans le shell parent. Dans votre code de test, vous avez:

echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935" | linesToArraySum

Les fonctions shell ne sont généralement pas exécutés dans des sous-shell, mais dans ce cas, vous exécutez linesToArraySum dans un sous-shell, car il apparaît dans une ligne de code. Dans certains autres shells, comme Ksh, la commande la plus à droite d'un pipeline n'est pas exécutée dans un sous-shell, et votre code fonctionnera réellement dans un tel shell. Mais Bash exécute même la dernière commande envoyée dans son propre sous-shell.

Étant donné que linesToArraySum est exécuté dans un sous-shell, le tableau arraySum n'existe que dans le sous-shell, est never créé pour l'appelant et est recréé récemment dans un nouveau sous-shell à chaque exécution du pipeline. En outre, même si le tableau existait déjà avant le démarrage du sous-shell, les modifications apportées dans le sous-shell ne modifieraient que la copie du sous-shell.

Tout ce que vous avez à faire pour résoudre ce problèmeconsiste à transmettre une entrée à linesToArraySum à l'aide d'une méthode qui n'exécute pas la fonction dans un sous-shell. Une façon de faire est d’utiliser un here string au lieu d’un pipeline:

linesToArraySum <<<"dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"

Vous pouvez l'utiliser comme solution de remplacement pour cette seule ligne de la boucle, bien que je vous suggère de remplacer votre boucle de test par ceci ou quelque chose de similaire:

for i in {0..9}; do linesToArraySum <<<"dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"; done

(Bien sûr, vous pouvez choisir de l'écrire sur plusieurs lignes.)


Comme Sergiy Kolodyazhnyy mentionne , vous appelez votre fonction une fois par ligne, au lieu de la transmettre à toutes les lignes. Le code fixe que j'ai montré ci-dessus ne change pas cela. Puisque vous avez écrit la fonction linesToArraySum pour lire plusieurs lignes, vous voudrez peut-être que votre code de test le teste. Mais c'est not ​​pourquoi les valeurs de arraySum ne sont pas conservées. Le premier script Bash de La réponse de Sergiy Kolodyazhnyy évite le problème en canalisant plusieurs lignes d’entrée à la fois vers la fonction Shell, de sorte que chaque modification du tableau ait lieu dans le même sous-shell. C'est pourquoi ça marche. En outre:

  • Une fois la commande generate_lines | sum_line_tokens terminée, les commandes suivantes ne pourront toujours pas lire les sommes provenant de arraySum, car le tableau est toujours créé dans un sous-shell détruit à la fin de la commande.
  • Tant que vous utilisez un pipeline, créer le tableau arraySumbefore appeler la fonction dans une ligne de commande ne fonctionnera pas pour conserver les valeurs, soit. Le Le sous-shell recevra une copie de arraySum de l'appelant. Le code qui s'exécute dans le sous-shell pourra donc accéder aux valeurs qui lui ont été attribuées. Toutefois, lorsqu'il écrit dans le tableau, cela n'affectera que la copie du sous-shell. Et si vous arrêtez d'appeler votre fonction dans une pipline, vous n'avez rien d'autre à faire pour que cela fonctionne!

Ce deuxième point mérite d’être expliqué, car il concerne un point commun de confusion. Dans Bash, x=foo; IFS= read -r x <<<bar; echo "$x" affiche bar, mais x=foo; echo bar | IFS= read -r x; echo "$x" imprime foo. Les placer dans une fonction, déclarer la variable avec declare ou local et/ou utiliser un tableau ne modifie pas le principe fondamental selon lequel modifier une variable dans un sous-shell ne la modifie pas pour l'appelant. Par exemple, supposons que vous exécutiez cette définition:

 f() { local -ai a=(10 20 30); g() { IFS= read -r 'a[3]'; echo "${a[@]}"; }; echo 40 | g; echo "${a[@]}"; }

Puis lancez f. La sortie indique que le tableau a est modifié dans le pipeline où la fonction g est appelée, mais la modification ne persiste pas après la commande echo 40 | g:

10 20 30 40
10 20 30

La raison pour laquelle le second script Bash de Sergiy Kolodyazhnyy answer est simplement qu'il évite d'utiliser un pipeline et évite ainsi d'exécuter sa fonction sum_line_tokens dans un sous-shell. Pour ce faire, les entrées sont redirigées à partir d’un fichier (< "$tempfile") au lieu d’utiliser un canal:

generate_lines > "$tempfile"
sum_line_tokens "$1" < "$tempfile"

Ce script contient un commentaire expliquant que sum_line_tokens sera exécuté dans un sous-shell si vous l'utilisez dans un pipeline, comme generate_lines | sum_line_tokens. Ce commentaire est en fait la réponse à toute votre question. Les autres modifications de ce script - écrire une fonction main(), créer explicitement le tableau avant d'appeler les fonctions qui l'utilisent , et l’utilisation de la variable local pour le faire ne sont absolument pas pertinents. (Ce script dans son ensemble est ​​reste utile, cependant, en ce sens qu'il montre un moyen d'éviter d'utiliser un pipeline et qu'il permet d'implémenter le comportement associé que vous avez demandé dans les commentaires.)

Lorsque vous renoncez à placer une commande dans une colonne pour l'empêcher de s'exécuter dans un sous-shell, l'option choisie dépend des circonstances. Pour le texte qui apparaît dans votre script, utilisez un ici chaîne (comme indiqué ci-dessus) ou ici document . Pour la sortie d'une autre commande, écrit vers un fichier temporaire puis reading de lui - comme dans Sergiy Kolodyazhnyy second script Bash - est souvent un choix raisonnable. Vous pouvez même créer le fichier temporaire en tant que nommé pipe avec mkfifo au lieu d'un fichier normal, si vous souhaitez qu'il soit identique. sémantique et caractéristiques de performance similaires à celles d'un pipeline Shell. Mais dans la plupart des cas, je recommande d'utiliser processus de substitution , qui crée, utilise et détruit un canal nommé pour vous, tous derrière les scènes:

sum_line_tokens "$1" < <(generate_lines)

Pour exécuter cette commande, le shell:

  • Crée un canal nommé temporaire.
  • Lance generate_lines et redirige sa sortie vers le canal nommé.
  • Remplace <(generate_lines) par le nom du canal nommé.
  • Exécute sum_line_tokens "$1" et redirige l’entrée du canal nommé (en raison de <).

La commande qui écrit dans le canal nommé est en fait exécutée en même temps que la commande lue dans le canal nommé. L'ordre donné ci-dessus concerne la facilité conceptuelle (je devais les écrire dans un ordre). Notez également que:

  • Le premier < pour redirection d'entrée et le second < qui fait partie de la syntaxe de substitution du processus doit ​​être séparés. Cela revient à dire que, où ... est la commande à partir de laquelle vous souhaitez prendre une entrée, écrivez < <(...), pas<<(...).
  • Substitution de processus ne utilise un sous-shell - mais seulement ​​pour le processus substitué. Ainsi, la commande generate_lines - is est exécuté dans un sous-shell, mais sum_line_tokens ne l’est pas. Si vous tentiez de modifier les variables de l'appelant dans generate_lines, ces modifications ne persisteraient pas par la suite. Cependant, generate_lines n'a pas à le faire. Seul sum_line_tokens doit modifier les variables qui seront utilisées ultérieurement. Il suffit donc que it ​​ne soit pas exécuté dans un sous-shell.
  • La substitution de processus - ainsi que les chaînes et [[-- ne sont pas portables pour tous Shells de style Bourne . (Ici, les documents et test/[ sont portables.) Mais les tableaux ne sont pas portables non plus, aussi longtemps que vous utilisez un tableau pour cela, vous n'écrivez pas déjà un disque portable. script - dans le sens où il est portable sur différents shells - il n’ya donc aucune raison pour que vous évitiez de recourir à la substitution de processus.

Il y a d'autres erreurs dans votre script. Comme ils sont faciles à créer dans n'importe quel script - pas seulement celui-ci - et comme je suppose que vous écrivez ce script à des fins pratiques, je les énumérerai ici. Cependant, en tant que Sergiy Kolodyazhnyy dit , vous devriez envisager d'utiliser un outil tel que awk pour ceci. De nombreux utilitaires Unix standard existent principalement à cette fin. traitement du texte ligne par ligne, et awk en fait partie.

Le traitement de texte avec une boucle Shell peut parfois être raisonnable et constitue même le meilleur choix. Mais pour presque toutes les tâches pouvant être effectuées avec un utilitaire standard, , il est préférable de le faire de cette façon plutôt que d'écrire une boucle while dans votre Shell qui utilise le script intégré read. Les shells sont glue languages ​​ et s'il y a une commande externe qui fait le travail, vous devriez l'utiliser.

Cela dit, je vous recommande d’améliorer ces autres parties du script si vous choisissez de continuer à l’utiliser:

  • Comme Sergiy Kolodyazhnyy dit , vous ne pouvez pas utiliser return pour retourner des tableaux. En fait, vous ne pouvez même pas renvoyer une variable simple. Vous pouvez uniquement renvoyer un code de sortie , lequel doit être compris entre 0 et 255 et n'est pas très polyvalent . Le principal objectif de passer un argument à la variable return ou exit est d’indiquer s’il ya eu ou non une erreur, ou laquelle de plusieurs erreurs possibles s’est produite, ou de renvoyer l’un des petite poignée pièces possibles. d'information. (Par exemple, le code de retour intégré dans test/[ indique si la condition testée est vraie ou fausse.) Avec le code que vous avez, vous devriez voir cette erreur:

    -bash: return: arraySum: numeric argument required
    
  • Vous devez passer -r lorsque vous utilisez la syntaxe read. Sinon, \ les échappements sont développés. Il est extrêmement rare que ce soit ce que vous voudriez. Utilisez donc read -r line au lieu de read line et utilisez read -ra arrayLog (ou read -r -a arrayLog si vous préférez ce style) à la place de read -a arrayLog.

  • Même pour lire une ligne dans une seule variable, définissez IFS= sauf si vous avez une raison spécifique pour laquelle vous savez que vous n’avez pas besoin (ou n’avez pas besoin de). Au lieu d'utiliser while read line, utilisez while IFS= read -r line. La raison en est que read supprime les espaces IFS - tout élément de $IFS-- à partir du début et de la fin de la ligne lue. Les exceptions sont si vous voulez réellement que cela se produise et --pour Bash - si vous omettez le nom de la variable. Dans Bash, read -r sans nom de variable équivaut à IFS= read -r REPLY.

  • Bien que cela ne soit pas faux en réalité, vous n'avez pas besoin d'utiliser la syntaxe d'expansion complète des paramètres dans (()) pour utiliser les valeurs de variables ou d'éléments de tableau. En évitant cela, ces expressions sont beaucoup plus faciles à lire. Préférez $((arraySum[n] + arrayLog[n])) par rapport à $((${arraySum[n]} + ${arrayLog[n]})).

  • Avec test, [ et [[, l'opérateur < effectue une comparaison lexicographique de chaînes et une comparaison numérique not. Pour vérifier si $i est inférieur à 9, vous pouvez utiliser [[ $i -lt 9 ]]. Par exemple, avec i=89, [[ $i < 9 ]] renvoie true! De même, , vous utiliseriez -gt pour numérique supérieur à, -le pour numérique inférieur ou égal, et -ge pour numérique supérieur à égal ou égal. Ou peut-être avez-vous voulu écrire (($i < 9)), qui fonctionnerait comme ((i < 9)).

    Cependant, puisque dans ce cas, vous souhaitez simplement passer de 1 à 9, il est beaucoup plus simple, plus clair et plus facile à utiliser une boucle for avec accolade ({1..9}) comme indiqué au début de cet article.

Enfin, je vous recommande de tirer parti de la puissance de analyse de code statique en vérifiant vos scripts de shell avec ShellCheck . ShellCheck détectera la plupart des erreurs répertoriées ci-dessus. Beaucoup de scripteurs expérimentés de Shell l'utilisent beaucoup, mais c'est également très utile pour les novices, car il contient des explications complètes pour chacune de ses règles.

Parfois, ShellCheck identifie quelque chose comme éventuellement faux qui est réellement correct. Par exemple, lorsque je l'ai exécuté sur votre script, il a généré SC2086 pour <<< $logLine. Strictement parlant, cela n’est pas nécessaire dans les versions de Bash fournies dans les systèmes Ubuntu actuellement pris en charge, car le texte à droite de <<< dans un ici chaîne n’est pas sujet à chemin. expansion ou fractionnement de Word. Cependant, les versions antérieures n'a pas ignoré ces extensions, plus c'est une bonne idée à citez vos variables chaque fois que vous n'avez pas de raison particulière de ne pas le faire. Ceci est un modèle courant: même avec certains des avertissements de ShellCheck que vous pouvez ignorer en toute sécurité, vous écrirez un meilleur code si vous choisissez de les respecter.

2
Eliah Kagan

J'ai pris la liberté d'éditer un peu votre fonction et de tout mettre dans un script simple. Le cœur du problème est que vous avez dû faire écho après la fin de la boucle while . De plus, les fonctions bash ne "renvoient" pas les tableaux, vous devez les répercuter sur stdout ou utiliser une fonction main et avoir un tableau local dans main, qui peut ensuite être accessible aux fonctions enfants (c’est quelque chose que je fais assez souvent dans mes propres scripts).

Voici un résultat de test. Pour 9 itérations avec la colonne 1 toujours égale à 110, on obtient 990.

$ ./generate_lines.sh                                                                                                       
990 1008 1035 1008 1017 1080

Et voici le script:

#!/usr/bin/env bash

sum_line_tokens() {
while read line
do
    #echo "$line"
    logLine=$line # saves currently logged line in variable logLine
    # redirect variable logLine as input for read command. 
    # read -a saves Word of input string as array. InternalFieldSeparator set as ';' 
    # detects elements in input string which are separated by '; ' as words.
    IFS=';' read -a arrayLog <<< $logLine     

    for n in 1 3 5 7 9 11
    do
        # define element in arraySum at position n as sum of previous element 
        # and element in arrayLog at this position
        arraySum[n]=$(( ${arraySum[n]} + ${arrayLog[n]} ))         
        #echo "${arraySum[n]}"
    done
done

# Functions in bash can only use return to indicate exit status
# This is more like int datatype for C or Java functions. If you want
# to return a string or array, you need to echo it to stdout
echo "${arraySum[@]}"
}

generate_lines(){
    while [[ $i < 9 ]] 
    do 
        i=$(($i + 1))
        echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"
    done
}

generate_lines |  sum_line_tokens

Simplifier la tâche avec awk

Tant que le script fonctionne, c'est long. Nous pouvons raccourcir la solution avec awk:

# again, same thing - the script now generates lines only, no summing. 
# We'll pipe it to awk
$ ./generate_lines.sh                                                                                                                                                                           
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935

$ ./generate_lines.sh  | awk -F ';' '{for(i=1;i<=11;i++) if(i%2 != 0) sum[i+1]+=$(i+1) }END{for(j in sum) printf "%d\t",sum[j];print ""}'                                                       
990 1008    1035    1008    1017    1080

Utilisation de la fonction principale et du tableau local

Je suis un ardent défenseur de l’utilisation des fonctions main dans les scripts, car cela permet de tout organiser et, en plus, vous pouvez déclarer une variable locale que chaque fonction appelée de main saura et sera capable d'écraser.

Eh bien, il y a un petit problème dans votre cas. Nous avons deux fonctions, l’une qui produit des lignes et l’autre - fait quelque chose avec ces lignes, et l’utilisation de tuyaux pose un problème - tout ce qui s’exécute sur le côté droit du tuyau s’exécute en sous-shell, ce qui signifie que toutes les informations du sous-shell disparaissent lorsque le sous-shell quitte le sous-shell. . Voir mon ancienne question pour référence.

Par conséquent, nous avons besoin d’un terrain neutre - nous utilisons soit un fichier temporaire, soit un nom appelé "tuyau nommé". Dans cet exemple, j'ai simplement utilisé un fichier temporaire. Si ce que vous devez analyser n’est pas trop volumineux, vous pouvez toujours tout stocker dans une variable locale et laisser deux fonctions s’occuper de la même variable - une écriture dans variable, l’autre - analyse de la variable. En cas de texte long pouvant contenir des milliers de lignes, il est préférable d’utiliser un fichier temporaire.

Donc, dans cette version du script, j'ai couvert plusieurs choses, y compris la fonction principale et la façon dont la fonction principale obtient les arguments en ligne de commande, ainsi que ce que vous avez demandé dans les commentaires. Fondamentalement, le script obtient désormais 1 argument de ligne de commande - le nombre de lignes souhaitées, et le donne à la fonction sum_line_tokens. Sans arguments de ligne de commande, toutes les lignes sont additionnées.

Essai:

$ ./generate_lines.sh 3                                                                                                                                                                         
330 336 345 336 339 360

$ ./generate_lines.sh 4                                                                                                                                                                         
440 448 460 448 452 480

Et le script lui-même:

#!/usr/bin/env bash

sum_line_tokens() {
    # To perform counting for n number of lines, use a counter variable
    # In this case I am using argument passed from command-line
    linecount=0


    # IFS= and -r for better line reading to ensure that spaces won't mess you up
    while IFS='' read -r line
    do
        # Check if we have arg 1 to function and quit after n lines
        if [ -n $1  ] && [ $linecount -eq $1 ] 
        then
            break
        fi

        logLine=$line 
        IFS=';' read -a arrayLog <<< $logLine     


        for n in 1 3 5 7 9 11
        do
            arraySum[n]=$(( ${arraySum[n]} + ${arrayLog[n]} ))         
        done
        # increment line counter
        ((linecount++))
    done
}

generate_lines(){
    while [[ $i < 9 ]] 
    do 
        i=$(($i + 1))
        echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"
    done
}

main(){
    # create local array. Any function called from main will know about it
    local -a arraySum

    # We can't just pipe lines to summing function. Whatever runs on the right-hand side
    # of the pipe runs in subshell, which means when that subshell exits, your variables are gone
    # See https://askubuntu.com/q/704154/295286
    tempfile=$(mktemp)
    generate_lines > "$tempfile"
    sum_line_tokens "$1" < "$tempfile"
    echo "${arraySum[@]}"
    rm "$tempfile"
}

# Call main function with the command-line arguments. This works sort of like int main(String[] args) in Java
main "$@"

Note sur la portabilité

Bien sûr, comme nous utilisons beaucoup de choses spécifiques à Bash, si vous utilisez ceci sur un système où bash n'est pas disponible, cela ne fonctionnera pas. Est-ce un bon script? Oui, ça fait le travail. Est-ce que ce script portable? La solution awk ci-dessus est probablement plus portable.

2
Sergiy Kolodyazhnyy