Come faccio a scorrere su un intervallo di numeri definiti dalle variabili in Bash?

Come faccio a scorrere su un intervallo di numeri in Bash quando l’intervallo è dato da una variabile?

So che posso farlo (chiamato “espressione di sequenza” nella documentazione di Bash):

for i in {1..5}; do echo $i; done 

Che dà:

1
2
3
4
5

Eppure, come posso sostituire uno degli endpoint di gamma con una variabile? Questo non funziona:

 END=5 for i in {1..$END}; do echo $i; done 

Quale stampa:

{1..5}

 for i in $(seq 1 $END); do echo $i; done 

edit: preferisco il seq rispetto agli altri metodi perché posso effettivamente ricordarlo;)

Il metodo seq è il più semplice, ma Bash ha una valutazione aritmetica integrata.

 END=5 for ((i=1;i<=END;i++)); do echo $i done # ==> outputs 1 2 3 4 5 on separate lines 

Il for ((expr1;expr2;expr3)); build funziona come for (expr1;expr2;expr3) in C e lingue simili, e come altri casi ((expr)) , Bash li considera come aritmetici.

discussione

Usare seq va bene, come suggerito da Jiaaro. Pax Diablo ha suggerito un ciclo Bash per evitare di chiamare un sottoprocesso, con l’ulteriore vantaggio di essere più adatto alla memoria se $ END è troppo grande. Zathrus ha individuato un tipico bug nell’implementazione del ciclo e ha anche lasciato intendere che, dal momento che i è una variabile di testo, le conversioni continue su e giù per i numeri vengono eseguite con un rallentamento associato.

intero aritmetico

Questa è una versione migliorata del ciclo Bash:

 typeset -ii END let END=5 i=1 while ((i<=END)); do echo $i … let i++ done 

Se l'unica cosa che vogliamo è l' echo , allora potremmo scrivere echo $((i++)) .

L'efemio mi ha insegnato qualcosa: Bash consente costrutti for ((expr;expr;expr)) . Dal momento che non ho mai letto tutta la pagina man per Bash (come ho fatto con la pagina man di Korn shell ( ksh ), e questo è stato tanto tempo fa), mi sono perso.

Così,

 typeset -ii END # Let's be explicit for ((i=1;i<=END;++i)); do echo $i; done 

sembra essere il modo più efficiente in termini di memoria (non sarà necessario allocare memoria per consumare l'output di seq , che potrebbe essere un problema se END è molto grande), anche se probabilmente non è il "più veloce".

la domanda iniziale

eschercycle ha notato che la notazione { a .. b } Bash funziona solo con i letterali; vero, di conseguenza al manuale di Bash. Si può superare questo ostacolo con una sola (interna) fork() senza exec() (come nel caso del chiamare seq , che essendo un'altra immagine richiede un fork + exec):

 for i in $(eval echo "{1..$END}"); do 

Sia eval che echo sono i builder di Bash, ma un fork() è necessario per la sostituzione di comando (il costrutto $(…) ).

Ecco perché l’espressione originale non ha funzionato.

Da uomo bash :

L’espansione della contrattura viene eseguita prima di ogni altra espansione e i caratteri speciali di altre espansioni vengono conservati nel risultato. È rigorosamente testuale. Bash non applica alcuna interpretazione sintattica al contesto dell’espansione o del testo tra parentesi graffe.

Quindi, l’ espansione delle parentesi è qualcosa che si fa presto come un’operazione macro puramente testuale, prima dell’espansione dei parametri.

I gusci sono ibridi altamente ottimizzati tra macro processori e linguaggi di programmazione più formali. Al fine di ottimizzare i casi d’uso tipici, la lingua è resa più complessa e alcune limitazioni sono accettate.

Raccomandazione

Suggerirei di attenersi alle funzionalità di Posix 1 . Questo significa usare for i in ; do for i in ; do , se la lista è già nota, altrimenti usa while o seq , come in:

 #!/bin/sh limit=4 i=1; while [ $i -le $limit ]; do echo $i i=$(($i + 1)) done # Or ----------------------- for i in $(seq 1 $limit); do echo $i done 

1. Bash è una grande shell e io la uso in modo interattivo, ma non metto bash-isms nei miei script. Gli script potrebbero necessitare di una shell più veloce, una più sicura, più uno in stile embedded. Potrebbe essere necessario eseguire qualsiasi cosa sia installata come / bin / sh, e quindi ci sono tutti i soliti argomenti pro-standard. Ricorda shellshock, aka bashdoor?

Il modo POSIX

Se ti interessa la portabilità, usa l’ esempio dello standard POSIX :

 i=2 end=5 while [ $i -le $end ]; do echo $i i=$(($i+1)) done 

Produzione:

 2 3 4 5 

Cose che non sono POSIX:

  • (( )) senza dollaro, sebbene sia un’estensione comune come menzionato dallo stesso POSIX .
  • [[ . [ è abbastanza qui. Vedi anche: Qual è la differenza tra parentesi quadre singole e doppie in Bash?
  • for ((;;))
  • seq (GNU Coreutils)
  • {start..end} , e questo non può funzionare con le variabili come menzionato dal manuale di Bash .
  • let i=i+1 : POSIX 7 2. Shell Command Language non contiene la parola let , e fallisce su bash --posix 4.3.42
  • potrebbe essere richiesto il dollaro a i=$i+1 , ma non sono sicuro. POSIX 7 2.6.4 Espansione aritmetica dice:

    Se la variabile di shell x contiene un valore che forma una costante intera valida, opzionalmente includendo un segno più o meno iniziale, le espansioni aritmetiche “$ ((x))” e “$ (($ x))” restituiranno lo stesso valore.

    ma leggendolo letteralmente ciò non implica che $((x+1)) espanda poiché x+1 non è una variabile.

Un altro strato di riferimento indiretto:

 for i in $(eval echo {1..$END}); do ∶ 

Puoi usare

 for i in $(seq $END); do echo $i; done 

Se sei su BSD / OS X puoi usare jot invece di seq:

 for i in $(jot $END); do echo $i; done 

Questo funziona bene in bash :

 END=5 i=1 ; while [[ $i -le $END ]] ; do echo $i ((i = i + 1)) done 

Se ti serve il prefisso di quanto ti potrebbe piacere

  for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done 

che cede

 07 08 09 10 11 12 

So che questa domanda riguarda bash , ma – solo per la cronaca – ksh93 è più intelligente e lo implementa come previsto:

 $ ksh -c 'i=5; for x in {1..$i}; do echo "$x"; done' 1 2 3 4 5 $ ksh -c 'echo $KSH_VERSION' Version JM 93u+ 2012-02-29 $ bash -c 'i=5; for x in {1..$i}; do echo "$x"; done' {1..5} 

Questo è un altro modo:

 end=5 for i in $(bash -c "echo {1..${end}}"); do echo $i; done 

Questi sono tutti carini ma seq è presumibilmente deprecato e la maggior parte funziona solo con intervalli numerici.

Se si racchiude il ciclo for tra virgolette doppie, le variabili di inizio e fine verranno cancellate quando si esegue il richiamo della stringa e sarà ansible inviare nuovamente la stringa a BASH per l’esecuzione. $i bisogno di essere scappato con \ s così non è valutato prima di essere inviato alla subshell.

 RANGE_START=a RANGE_END=z echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash 

Questo output può anche essere assegnato a una variabile:

 VAR=`echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash` 

L’unico “overhead” che dovrebbe generare dovrebbe essere la seconda istanza di bash, quindi dovrebbe essere adatta per operazioni intensive.

Sostituisci {} con (( )) :

 tmpstart=0; tmpend=4; for (( i=$tmpstart; i<=$tmpend; i++ )) ; do echo $i ; done 

I rendimenti:

 0 1 2 3 4 

Se stai facendo i comandi della shell e tu (come me) hai un feticcio per il pipelining, questo è buono:

seq 1 $END | xargs -I {} echo {}

Se vuoi rimanere il più vicino ansible alla syntax dell’espressione di controvento, prova la funzione range da range.bash bash-tricks .

Ad esempio, tutto quanto segue farà esattamente la stessa cosa di echo {1..10} :

 source range.bash one=1 ten=10 range {$one..$ten} range $one $ten range {1..$ten} range {1..10} 

Prova a supportare la syntax di bash nativa con il minor numero ansible di “raggiri”: non solo sono supportate le variabili, ma il comportamento spesso indesiderato degli intervalli non validi viene fornito come stringhe (ad esempio for i in {1..a}; do echo $i; done ) è anche prevenuto.

Le altre risposte funzioneranno nella maggior parte dei casi, ma hanno tutti almeno uno dei seguenti inconvenienti:

  • Molti usano subshells , che possono danneggiare le prestazioni e potrebbero non essere possibili su alcuni sistemi.
  • Molti di loro si basano su programmi esterni. Anche seq è un file binario che deve essere installato per essere utilizzato, deve essere caricato da bash e deve contenere il programma che ci si aspetta, affinché funzioni in questo caso. Onnipresente o no, è molto più affidabile del semplice linguaggio Bash.
  • Le soluzioni che utilizzano solo la funzionalità Bash nativa, come @ ephemient, non funzioneranno su intervalli alfabetici, come {a..z} ; rinforzare l’espansione. La domanda era su intervalli di numeri , però, quindi questo è un cavillo.
  • Molti di questi non sono visivamente simili alla syntax dell’intervallo di parentesi {1..10} , quindi i programmi che usano entrambi possono essere leggermente più difficili da leggere.
  • La risposta di @bobbogo utilizza una syntax familiare, ma fa qualcosa di inaspettato se la variabile $END non è un intervallo valido “bookend” per l’altro lato dell’intervallo. Se END=a , ad esempio, non si verificherà un errore e il valore letterale {1..a} verrà echeggiato. Questo è anche il comportamento predefinito di Bash – è solo spesso inaspettato.

Disclaimer: sono l’autore del codice collegato.

Questo funziona in Bash e Korn, inoltre può passare da numeri più alti a numeri più bassi. Probabilmente non è il più veloce o il più bello ma funziona abbastanza bene. Gestisce anche i negativi.

 function num_range { # Return a range of whole numbers from beginning value to ending value. # >>> num_range start end # start: Whole number to start with. # end: Whole number to end with. typeset sev s=${1} e=${2} if (( ${e} >= ${s} )); then v=${s} while (( ${v} <= ${e} )); do echo ${v} ((v=v+1)) done elif (( ${e} < ${s} )); then v=${s} while (( ${v} >= ${e} )); do echo ${v} ((v=v-1)) done fi } function test_num_range { num_range 1 3 | egrep "1|2|3" | assert_lc 3 num_range 1 3 | head -1 | assert_eq 1 num_range -1 1 | head -1 | assert_eq "-1" num_range 3 1 | egrep "1|2|3" | assert_lc 3 num_range 3 1 | head -1 | assert_eq 3 num_range 1 -1 | tail -1 | assert_eq "-1" }