shell - script - faire une somme en bash




Commande Shell pour additionner des entiers, un par ligne? (20)

Je suis à la recherche d'une commande qui acceptera en entrée plusieurs lignes de texte, chaque ligne contenant un seul entier, et affichera la somme de ces entiers.

Comme un peu de fond, j'ai un fichier journal qui comprend des mesures de synchronisation, donc par le biais de grepping pour les lignes pertinentes, et un peu de reformatage sed je peux énumérer tous les timings dans ce fichier. Je voudrais travailler le total cependant, et mon esprit est devenu vide quant à n'importe quelle commande que je peux diriger cette sortie intermédiaire afin de faire la somme finale. J'ai toujours utilisé expr dans le passé, mais à moins qu'il ne fonctionne en RPN mode je ne pense pas qu'il va faire face à cela (et même alors, ce serait difficile).

Qu'est-ce que je rate? Étant donné qu'il y a probablement plusieurs façons d'y parvenir, je serai heureux de lire (et d' upvote ) toute approche qui fonctionne, même si quelqu'un d'autre a déjà posté une solution différente qui fait le travail.

Question connexe: Commande la plus courte pour calculer la somme d'une colonne de sortie sous Unix? (crédits @Andrew )

Mise à jour : Wow, comme prévu, il y a quelques bonnes réponses ici. On dirait que je vais certainement devoir faire une inspection plus approfondie d' awk comme un command-line tool en général!


AWK a déjà été mentionné, donc en plus je voudrais suggérer que vous utilisiez ce langage au lieu de GREP et SED pour analyser le fichier journal d'origine. Un script AWK approprié peut facilement faire le travail des deux et calculer la valeur intéressante comme Paul et Alf l'ont déjà souligné.


Avec jq :

seq 10 | jq -s 'add' # 'add' is equivalent to 'reduce .[] as $item (0; . + $item)'

Bash pur et court.

f=$(cat numbers.txt)
echo $(( ${f//$'\n'/+} ))

Bash pur et dans un one-liner :-)

$ cat numbers.txt
1
2
3
4
5
6
7
8
9
10


$ I=0; for N in $(cat numbers.txt); do I=$(($I + $N)); done; echo $I
55

Ce qui suit devrait fonctionner (en supposant que votre numéro est le deuxième champ sur chaque ligne).

awk 'BEGIN {sum=0} \
 {sum=sum + $2} \
END {print "tot:", sum}' Yourinputfile.txt

Coller fusionne généralement des lignes de plusieurs fichiers, mais il peut également être utilisé pour convertir des lignes individuelles d'un fichier en une seule ligne. L'indicateur delimiter vous permet de passer une équation de type x + x à bc.

paste -s -d+ infile | bc

Alternativement, lors de la tuyauterie de stdin,

<commands> | paste -s -d+ - | bc

J'ai fait un benchmark rapide sur les réponses existantes

  • utiliser seulement des outils standards (désolé pour des trucs comme lua ou rocket ),
  • sont de vraies doublures,
  • sont capables d'ajouter d'énormes quantités de chiffres (100 millions), et
  • sont rapides (j'ai ignoré ceux qui ont pris plus d'une minute).

J'ai toujours ajouté les nombres de 1 à 100 millions qui étaient faisables sur ma machine en moins d'une minute pour plusieurs solutions.

Voici les résultats:

Python

:; seq 100000000 | python -c 'import sys; print sum(map(int, sys.stdin))'
5000000050000000
# 30s
:; seq 100000000 | python -c 'import sys; print sum(int(s) for s in sys.stdin)'
5000000050000000
# 38s
:; seq 100000000 | python3 -c 'import sys; print(sum(int(s) for s in sys.stdin))'
5000000050000000
# 27s
:; seq 100000000 | python3 -c 'import sys; print(sum(map(int, sys.stdin)))'
5000000050000000
# 22s
:; seq 100000000 | pypy -c 'import sys; print(sum(map(int, sys.stdin)))'
5000000050000000
# 11s
:; seq 100000000 | pypy -c 'import sys; print(sum(int(s) for s in sys.stdin))'
5000000050000000
# 11s

Awk

:; seq 100000000 | awk '{s+=$1} END {print s}'
5000000050000000
# 22s

Pâte et Bc

Cela a manqué de mémoire sur ma machine. Il a fonctionné pour la moitié de la taille de l'entrée (50 millions de numéros):

:; seq 50000000 | paste -s -d+ - | bc
1250000025000000
# 17s
:; seq 50000001 100000000 | paste -s -d+ - | bc
3750000025000000
# 18s

Donc je suppose que cela aurait pris ~ 35s pour les 100 millions de numéros.

Perl

:; seq 100000000 | perl -lne '$x += $_; END { print $x; }'
5000000050000000
# 15s
:; seq 100000000 | perl -e 'map {$x += $_} <> and print $x'
5000000050000000
# 48s

Rubis

:; seq 100000000 | ruby -e "puts ARGF.map(&:to_i).inject(&:+)"
5000000050000000
# 30s

C

Juste pour l'amour de la comparaison, j'ai compilé la version C et testé cela aussi, juste pour avoir une idée de la lenteur des solutions basées sur les outils.

#include <stdio.h>
int main(int argc, char** argv) {
    long sum = 0;
    long i = 0;
    while(scanf("%ld", &i) == 1) {
        sum = sum + i;
    }
    printf("%ld\n", sum);
    return 0;
}

:; seq 100000000 | ./a.out 
5000000050000000
# 8s

Conclusion

C est bien sûr le plus rapide avec 8s, mais la solution de Pypy ajoute seulement une très petite surcharge d'environ 30% à 11s . Mais, pour être juste, Pypy n'est pas exactement standard. La plupart des utilisateurs n'ont installé que CPython, ce qui est nettement plus lent (22s), exactement aussi rapide que la populaire solution Awk.

La solution la plus rapide basée sur des outils standard est Perl (15s).


Je mettrais un gros avertissement sur la solution communément approuvée:

awk '{s+=$1} END {print s}' mydatafile # DO NOT USE THIS!!

c'est parce que dans cette forme awk utilise une représentation entière signée de 32 bits: elle débordera pour des sommes qui dépassent 2147483647 (ie, 2 ^ 31).

Une réponse plus générale (pour additionner des entiers) serait:

awk '{s+=$1} END {printf "%.0f\n", s}' mydatafile # USE THIS INSTEAD

PS J'aurais aimé commenter la première réponse, mais je n'ai pas assez de réputation ..


Je réalise que c'est une vieille question, mais j'aime cette solution assez pour la partager.

% cat > numbers.txt
1 
2 
3 
4 
5
^D
% cat numbers.txt | perl -lpe '$c+=$_}{$_=$c'
15

S'il y a un intérêt, je vais vous expliquer comment cela fonctionne.


La sommation en temps réel pour vous permettre de surveiller la progression de certaines tâches de calcul de nombres.

$ cat numbers.txt 
1
2
3
4
5
6
7
8
9
10

$ cat numbers.txt | while read new; do total=$(($total + $new)); echo $total; done
1
3
6
10
15
21
28
36
45
55

(Il n'est pas nécessaire de mettre $total à zéro dans ce cas.) Vous ne pouvez pas non plus accéder à $ total après l'arrivée.)


Les travaux suivants dans bash:

I=0

for N in `cat numbers.txt`
do
    I=`expr $I + $N`
done

echo $I

Ma version:

seq -5 10 | xargs printf "- - %s" | xargs  | bc

One-liner dans la raquette:

racket -e '(define (g) (define i (read)) (if (eof-object? i) empty (cons i (g)))) (foldr + 0 (g))' < numlist.txt

Perl pur alternatif, assez lisible, aucun paquet ou option requis:

perl -e "map {$x += $_} <> and print $x" < infile.txt

Solution BASH, si vous voulez en faire une commande (par exemple, si vous avez besoin de le faire fréquemment):

function addnums {
  TOTAL=0
  while read val; do
    TOTAL=$(( TOTAL + val))
  done
  echo $TOTAL
}

Ensuite, utilisez:

addnums < /tmp/nums

Un peu d'awk devrait le faire?

awk '{s+=$1} END {print s}' mydatafile

Note: certaines versions d'awk ont ​​des comportements bizarres si vous allez ajouter quelque chose dépassant 2 ^ 31 (2147483647). Voir les commentaires pour plus d'informations. Une suggestion est d'utiliser printf plutôt que d' print :

awk '{s+=$1} END {printf "%.0f", s}' mydatafile

Vous pouvez utiliser num-utils, bien qu'il puisse être excessif pour ce dont vous avez besoin. Ceci est un ensemble de programmes pour manipuler des nombres dans le shell, et peut faire plusieurs choses intéressantes, y compris bien sûr, en les additionnant. C'est un peu dépassé, mais ils fonctionnent toujours et peuvent être utiles si vous avez besoin de faire quelque chose de plus.

http://suso.suso.org/programs/num-utils/


Vous pouvez utiliser votre commande 'expr' préférée, il vous suffit de finaliser l'entrée en premier:

seq 10 | tr '[\n]' '+' | sed -e 's/+/ + /g' -e's/ + $/\n/' | xargs expr

Le processus est:

  • "tr" remplace les caractères eoln par un symbole +,
  • sed tamponne le '+' avec des espaces de chaque côté, puis retire le + final de la ligne
  • xargs insère l'entrée piped dans la ligne de commande pour expr à consommer.

dc -f infile -e '[+z1<r]srz1<rp'

Notez que les nombres négatifs précédés d'un signe moins devraient être traduits pour dc , car il utilise _ préfixe plutôt que - préfixe pour cela. Par exemple, via tr '-' '_' | dc -f- -e '...' tr '-' '_' | dc -f- -e '...' .

Edit: Puisque cette réponse a eu autant de votes "pour l'obscurité", voici une explication détaillée:

L'expression [+z1<r]srz1<rp fait ce qui suit :

[   interpret everything to the next ] as a string
  +   push two values off the stack, add them and push the result
  z   push the current stack depth
  1   push one
  <r  pop two values and execute register r if the original top-of-stack (1)
      is smaller
]   end of the string, will push the whole thing to the stack
sr  pop a value (the string above) and store it in register r
z   push the current stack depth again
1   push 1
<r  pop two values and execute register r if the original top-of-stack (1)
    is smaller
p   print the current top-of-stack

Comme pseudo-code:

  1. Définir "add_top_of_stack" comme:
    1. Supprimez les deux valeurs supérieures de la pile et ajoutez le résultat
    2. Si la pile a deux valeurs ou plus, exécutez récursivement "add_top_of_stack"
  2. Si la pile a deux valeurs ou plus, exécutez "add_top_of_stack"
  3. Imprime le résultat, maintenant le seul élément restant dans la pile

Pour vraiment comprendre la simplicité et la puissance de dc , voici un script Python qui implémente certaines des commandes de dc et exécute une version Python de la commande ci-dessus:

### Implement some commands from dc
registers = {'r': None}
stack = []
def add():
    stack.append(stack.pop() + stack.pop())
def z():
    stack.append(len(stack))
def less(reg):
    if stack.pop() < stack.pop():
        registers[reg]()
def store(reg):
    registers[reg] = stack.pop()
def p():
    print stack[-1]

### Python version of the dc command above

# The equivalent to -f: read a file and push every line to the stack
import fileinput
for line in fileinput.input():
    stack.append(int(line.strip()))

def cmd():
    add()
    z()
    stack.append(1)
    less('r')

stack.append(cmd)
store('r')
z()
stack.append(1)
less('r')
p()

perl -lne '$x += $_; END { print $x; }' < infile.txt






shell