Usando "while read ...", echo e printf ottengono risultati diversi


14

Secondo questa domanda " Usare" mentre leggi ... "in uno script di Linux "

echo '1 2 3 4 5 6' | while read a b c;do echo "$a, $b, $c"; done

risultato:

1, 2, 3 4 5 6

ma quando lo sostituisco echoconprintf

echo '1 2 3 4 5 6' | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done

risultato

1, 2, 3
4, 5, 6

Qualcuno potrebbe dirmi cosa rende questi due comandi diversi? grazie ~

Risposte:


24

Non è solo echo vs printf

Innanzitutto, capiamo cosa succede con la read a b cparte. readeseguirà la suddivisione delle parole in base al valore predefinito della IFSvariabile che è spazio-tab-newline e si adatterà a tutto in base a quello. Se c'è più input rispetto alle variabili per tenerlo, si adatterà le parti divise nelle prime variabili e ciò che non può essere adattato - entrerà per ultimo. Ecco cosa intendo:

bash-4.3$ read a b c <<< "one two three four"
bash-4.3$ echo $a
one
bash-4.3$ echo $b
two
bash-4.3$ echo $c
three four

Questo è esattamente come è descritto nel bashmanuale (vedi la citazione alla fine della risposta).

Nel tuo caso, ciò che accade è che 1 e 2 si adattano alle variabili aeb, e c prende tutto il resto, che è 3 4 5 6.

Quello che vedrai anche molte volte è che le persone usano while IFS= read -r line; do ... ; done < input.txtleggere file di testo riga per riga. Ancora una volta, IFS=è qui per un motivo per controllare la suddivisione delle parole, o più specificamente - disabilitarlo e leggere una singola riga di testo in una variabile. Se non ci readfosse, cercherebbe di adattare ogni singola parola alla linevariabile. Ma questa è un'altra storia, che ti incoraggio a studiare in seguito, poiché while IFS= read -r variableè una struttura molto usata.

comportamento echo vs printf

echofa quello che ti aspetteresti qui. Visualizza le variabili esattamente come le readha disposte. Ciò è già stato dimostrato nella discussione precedente.

printfè molto speciale, perché continuerà ad adattare le variabili alla stringa di formato fino a quando tutte non saranno esaurite. Quindi quando lo fai printf "%d, %d, %d \n" $a $b $cprintf vede una stringa di formato con 3 decimali, ma ci sono più argomenti di 3 (perché le tue variabili si espandono effettivamente a singoli 1,2,3,4,5,6). Questo può sembrare confuso, ma esiste per una ragione come un comportamento migliorato rispetto a quello che fa la vera printf() funzione in linguaggio C.

Quello che hai fatto anche qui che influenza l'output è che le tue variabili non sono quotate, il che consente alla shell (non printf) di scomporre le variabili in 6 elementi separati. Confronta questo con la citazione:

bash-4.3$ read a b c <<< "1 2 3 4"
bash-4.3$ printf "%d %d %d\n" "$a" "$b" "$c"
bash: printf: 3 4: invalid number
1 2 3

Esattamente perché la $cvariabile è quotata, ora è riconosciuta come una stringa intera 3 4e non si adatta al %dformato, che è solo un singolo numero intero

Ora fai lo stesso senza citare:

bash-4.3$ printf "%d %d %d\n" $a $b $c
1 2 3
4 0 0

printf dice ancora: "OK, ci sono 6 elementi lì ma il formato ne mostra solo 3, quindi continuerò a sistemare le cose e lascerò vuoto tutto ciò che non posso abbinare all'input effettivo dell'utente".

E in tutti questi casi non devi credermi sulla parola. Basta correre strace -e trace=execvee vedere di persona cosa "vede" effettivamente il comando:

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" $a $b $c
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3", "4"], [/* 80 vars */]) = 0
1 2 3
4 0 0
+++ exited with 0 +++

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" "$a" "$b" "$c"
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3 4"], [/* 80 vars */]) = 0
1 2 printf: 3 4’: value not completely converted
3
+++ exited with 1 +++

Note aggiuntive

Come Charles Duffy ha giustamente sottolineato nei commenti, bashha il suo built-in printf, che è quello che stai usando nel tuo comando, stracechiamerà effettivamente la /usr/bin/printfversione, non la versione della shell. A parte piccole differenze, per il nostro interesse in questa particolare domanda gli identificatori di formato standard sono gli stessi e il comportamento è lo stesso.

Ciò che dovrebbe anche essere tenuto presente è che la printfsintassi è molto più portatile (e quindi preferita) di echo, per non parlare del fatto che la sintassi è più familiare a C o a qualsiasi linguaggio simile a C che ha printf()funzione in essa. Vedere questo eccellente risposta di terdon sul tema della printfvs echo. Sebbene sia possibile personalizzare l'output in base alla propria shell specifica sulla versione specifica di Ubuntu, se si esegue il porting di script su sistemi diversi, probabilmente si dovrebbe preferire printfpiuttosto che l'eco. Forse sei un amministratore di sistema principiante che lavora con macchine Ubuntu e CentOS, o forse anche FreeBSD - chi lo sa - quindi in questi casi dovrai fare delle scelte.

Citazione dal manuale di bash, sezione SHELL BUILTIN COMMANDS

leggi [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ... ]

Una riga viene letta dall'input standard o dal descrittore di file fd fornito come argomento all'opzione -u, e la prima parola viene assegnata al primo nome, la seconda parola al secondo nome e così via, con gli avanzi le parole e i loro separatori intermedi assegnati al cognome. Se il flusso di input legge un numero inferiore di parole rispetto ai nomi, ai nomi rimanenti vengono assegnati valori vuoti. I caratteri in IFS sono usati per dividere la linea in parole usando le stesse regole che la shell usa per l'espansione (descritte sopra in Word Splitting).


2
Una notevole differenza tra il stracecaso e l'altro - strace printfsta usando /usr/bin/printf, mentre printfdirettamente in bash sta usando la shell incorporata con lo stesso nome. Non saranno sempre identici - ad esempio, l'istanza di bash ha identificatori di formato %qe, nelle nuove versioni, $()Tper la formattazione dell'ora.
Charles Duffy,

7

Questo è solo un suggerimento e non intende affatto sostituire la risposta di Sergiy. Penso che Sergiy abbia scritto un'ottima risposta sul perché sono diversi nella stampa. Come la variabile sulla lettura viene assegnata con il resto nella $cvariabile come 3 4 5 6dopo 1e 2sono assegnati a ae b. echonon dividerà la variabile per te dove printfsarà con la %ds.

Puoi, tuttavia, far sì che in sostanza ti forniscano le stesse risposte manipolando l'eco dei numeri all'inizio del comando:

In /bin/bashpuoi usare:

echo -e "1 2 3 \n4 5 6"

In /bin/shpuoi semplicemente usare:

echo "1 2 3 \n4 5 6"

Bash usa -e per abilitare i caratteri \ escape dove sh non ne ha bisogno in quanto è già abilitato. \nfa sì che crei una nuova linea, quindi ora la linea di eco è divisa in due linee separate che ora possono essere usate due volte per l'istruzione del ciclo di eco:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6

Che a sua volta produce lo stesso output usando il printfcomando:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

Nel sh

$ echo "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6
$ echo "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

Spero che sia di aiuto!


echo -enon è solo un'estensione di POSIX, ma qualsiasi echocosa non emetta -esul suo output dato che l'input sta effettivamente sfidando / rompendo le specifiche della lettera nera. (bash non è conforme per impostazione predefinita, ma è conforme allo standard quando sono impostati entrambi posixe i xpg_echoflag; quest'ultimo può essere reso predefinito in fase di compilazione, il che significa che non tutte le versioni di bash funzioneranno echo -ecome previsto qui).
Charles Duffy,

Vedi le sezioni USATION APPLICATION e RATIONALE delle specifiche POSIX per echosu pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html , descrivendo esplicitamente che echoil comportamento non è specificato in presenza di uno -no backslash in qualsiasi punto in input e avvisando che printfessere usato invece.
Charles Duffy,

@CharlesDuffy Ho mai scritto nel mio che mi aspetto che funzioni in tutte le versioni di bash? E che problema hai con la mia risposta? Se ritieni di avere una soluzione migliore, scrivila come una risposta invece di provare a dimostrare alla gente di sbagliarsi. Ho passato così tante volte con persone come te, quindi per favore basta con questi commenti come questo. Questo è tutto ciò che ho da dire.
Terrance,

3
@CharlesDuffy in Chiedi a Ubuntu a volte scriviamo risposte non trasportabili ad altre distro Linux. Dato che il tuo rappresentante qui è solo 103 punti potresti non averlo notato. Il tuo rappresentante su Stack Overflow è di 123.3K punti, quindi ovviamente conosci un sacco di cose sui sistemi * NIX in generale. Penso che tu, Terrace e Serg stiate bene ma guardiate il filo da diverse angolazioni. Mi associo (nessun gioco di parole) al sentimento di scrivere una risposta che sia * NIX portatile, o almeno portatile su distribuzioni Linux.
WinEunuuchs2Unix

2
@CharlesDuffy Mentre sono d'accordo con le tue risposte al sentiment, dovresti renderlo portatile, dopo tutto, questo è un sito orientato a Ubuntu, quindi gli strumenti specifici di Ubuntu dovrebbero essere assunti dagli utenti. Quindi l'avvertimento è in qualche modo implicito. Non entriamo in discussioni troppo lunghe. Hai ragione a considerare altre conchiglie e anche i neofiti che leggono per imparare dalle risposte dovrebbero essere considerati. Un breve commento sarebbe probabilmente sufficiente per chiarire il punto.
Sergiy Kolodyazhnyy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.