Come leggere da un file o STDIN in Bash?


245

Il seguente script Perl ( my.pl) può leggere dal file sulla riga di comando args o da STDIN:

while (<>) {
   print($_);
}

perl my.plleggerà da STDIN, mentre perl my.pl a.txtleggerà da a.txt. Questo è molto conveniente

Ti chiedi c'è un equivalente in Bash?

Risposte:


410

La seguente soluzione legge da un file se lo script viene chiamato con un nome file come primo parametro, $1altrimenti dall'input standard.

while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

La sostituzione ${1:-...}prende $1se definito, altrimenti viene utilizzato il nome file dell'input standard del proprio processo.


1
Bello, funziona. Un'altra domanda è perché aggiungi un preventivo per questo? "$ {1: - / proc / $ {$} / fd / 0}"
Dagang

15
Il nome file fornito nella riga di comando potrebbe contenere spazi vuoti.
Fritz G. Mehner,

3
C'è qualche differenza tra usare /proc/$$/fd/0e /dev/stdin? Ho notato che quest'ultimo sembra essere più comune e sembra più semplice.
Conoscenza del

19
Meglio aggiungere -ral tuo readcomando, in modo che non mangi accidentalmente i \ caratteri; utilizzare while IFS= read -r lineper conservare spazi bianchi iniziali e finali.
mklement0,

1
@NeDark: è curioso; Ho appena verificato che funziona su quella piattaforma, anche quando si usa /bin/sh- stai usando una shell diversa da basho sh?
mklement0

119

Forse la soluzione più semplice è reindirizzare stdin con un operatore di reindirizzamento unito:

#!/bin/bash
less <&0

Stdin è il descrittore di file zero. Quanto sopra invia l'input reindirizzato al tuo script bash nello stdin di less.

Ulteriori informazioni sul reindirizzamento del descrittore di file .


1
Vorrei avere più voti da darti, lo cerco da anni.
Marcus Downing,

13
Non c'è alcun vantaggio nell'usare <&0in questa situazione - il tuo esempio funzionerà allo stesso modo con o senza di esso - apparentemente, gli strumenti che invochi all'interno di uno script bash di default vedono lo stesso stdin dello script stesso (a meno che lo script non lo consumi per primo).
mklement0,

@ mkelement0 Quindi se uno strumento legge metà del buffer di input, lo strumento successivo che invoco otterrà il resto?
Asad Saeeduddin,

"Nome file mancante (" less --help "per assistenza)" quando
eseguo

5
dov'è la parte "o dal file" in questa risposta?
Sebastian,

85

Ecco il modo più semplice:

#!/bin/sh
cat -

Uso:

$ echo test | sh my_script.sh
test

Per assegnare stdin alla variabile, puoi usare: STDIN=$(cat -)o semplicemente STDIN=$(cat)come operatore non è necessario (come da commento @ mklement0 ).


Per analizzare ciascuna riga dall'input standard , provare il seguente script:

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done

Per leggere dal file o stdin (se l'argomento non è presente), è possibile estenderlo a:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")

Appunti:

- read -r- Non trattare un carattere barra rovesciata in alcun modo speciale. Considerare ogni barra rovesciata come parte della riga di input.

- Senza impostazione IFS, per impostazione predefinita le sequenze di Spacee Taball'inizio e alla fine delle righe vengono ignorate (tagliate).

- Uso printfinvece di echoevitare di stampare righe vuote quando la linea è costituita da un singolo -e, -no -E. Tuttavia, esiste una soluzione alternativa env POSIXLY_CORRECT=1 echo "$line"che utilizza l' esecuzione della GNU esternaecho che la supporta. Vedi: Come echo "-e"?

Vedi: Come leggere stdin quando non vengono passati argomenti? su stackoverflow SE


Si potrebbe semplificare [ "$1" ] && FILE=$1 || FILE="-"al FILE=${1:--}. (Quibble: meglio evitare variabili shell maiuscole per evitare collisioni di nomi con variabili d' ambiente .)
mklement0

Il piacere è tutto mio; in realtà, ${1:--} è conforme a POSIX, quindi dovrebbe funzionare in tutte le shell simili a POSIX. Ciò che non funzionerà in tutte queste shell è la sostituzione dei processi ( <(...)); funzionerà in bash, ksh, zsh, ma non in trattino, per esempio. Inoltre, è meglio aggiungere -ral tuo readcomando, in modo che non mangi accidentalmente i \ caratteri; anteponi IFS= per preservare gli spazi bianchi iniziali e finali.
mklement0,

4
In realtà il tuo codice si interrompe ancora a causa di echo: se una riga è composta da -e, -no -E, non verrà mostrata. Per risolvere questo problema, è necessario utilizzare printf: printf '%s\n' "$line". Non l'ho incluso nella mia precedente modifica ... troppo spesso le mie modifiche vengono ripristinate quando correggo questo errore :(.
gniourf_gniourf

1
No, non fallisce. Ed --è inutile se il primo argomento è'%s\n'
gniourf_gniourf

1
La tua risposta va bene per me (voglio dire, non sono più a conoscenza di bug o funzionalità indesiderate), sebbene non tratti più argomenti come Perl. In effetti, se vuoi gestire più argomenti, finirai per scrivere l'eccellente risposta di Jonathan Leffler - in effetti la tua sarebbe migliore dal momento che avresti usato IFS=con reade printfinvece di echo. :).
gniourf_gniourf

19

Penso che questo sia il modo semplice:

$ cat reader.sh
#!/bin/bash
while read line; do
  echo "reading: ${line}"
done < /dev/stdin

-

$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
  echo "line ${i}"
done

-

$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5

4
Questo non soddisfa il requisito del poster per la lettura da stdin o da un argomento di file, si legge solo da stdin.
nash

3
Lasciando @ valida obiezione di Nash a parte: readlegge da stdin di default , quindi non c'è alcun bisogno per < /dev/stdin.
mklement0,

13

La echosoluzione aggiunge nuove righe ogni volta che IFSinterrompe il flusso di input. La risposta di @ fgm può essere modificata un po ':

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"

Potresti spiegare cosa intendi con "soluzione echo aggiunge nuove righe ogni volta che IFS interrompe il flusso di input"? Nel caso in cui si stia riferendo a read's comportamento: mentre read non potenzialmente suddiviso in più tokens sulla base dei caratteri. contenuto in $IFS, restituisce un singolo token solo se si specifica un solo nome di variabile (ma per impostazione predefinita vengono tagliati e spazi bianchi iniziali e finali).
mklement0,

@ mklement0 Concordo con te al 100% sul comportamento di reade $IFS- a sua echovolta aggiunge nuove righe senza la -nbandiera. "L'utilità echo scrive tutti gli operandi specificati, separati da singoli caratteri vuoti (` ') e seguiti da un carattere di nuova riga (`\ n'), nell'output standard."
David Souther,

Fatto. Tuttavia, per emulare il ciclo Perl è necessario il trailing \naggiunto da echo: Perl's $_ include la fine \ndella riga dalla riga letta, mentre bash's readno. (Tuttavia, come sottolinea @gniourf_gniourf altrove, l'approccio più solido è quello di utilizzare printf '%s\n'al posto di echo).
mklement0

8

Il ciclo Perl nella domanda legge da tutti gli argomenti del nome file sulla riga di comando o dall'input standard se non viene specificato alcun file. Le risposte che vedo sembrano elaborare un singolo file o input standard se non è stato specificato alcun file.

Anche se spesso deriso accuratamente come UUOC (Uso inutile di cat), ci sono momenti in cui catè lo strumento migliore per il lavoro ed è discutibile che questo sia uno di questi:

cat "$@" |
while read -r line
do
    echo "$line"
done

L'unico aspetto negativo di questo è che crea una pipeline in esecuzione in una sotto-shell, quindi cose come le assegnazioni di variabili nel whileciclo non sono accessibili al di fuori della pipeline. Il bashmodo per aggirare questo è Sostituzione processo :

while read -r line
do
    echo "$line"
done < <(cat "$@")

Questo lascia il whileloop in esecuzione nella shell principale, quindi le variabili impostate nel loop sono accessibili al di fuori del loop.


1
Ottimo punto su più file. Non so quali sarebbero le implicazioni di risorse e prestazioni, ma se non sei su bash, ksh o zsh e quindi non puoi usare la sostituzione di processo, potresti provare un here-doc con sostituzione di comando (distribuito su 3 linee) >>EOF\n$(cat "$@")\nEOF. Infine, un cavillo: while IFS= read -r lineè una migliore approssimazione di ciò che while (<>)fa in Perl (conserva gli spazi bianchi iniziali e finali, sebbene Perl mantenga anche il finale \n).
mklement0,

4

Il comportamento di Perl, con il codice indicato nell'OP, può accettare nessuno o più argomenti e se un argomento è un singolo trattino, -questo viene inteso come stdin. Inoltre, è sempre possibile avere il nome file con $ARGV. Nessuna delle risposte fornite finora imita davvero il comportamento di Perl sotto questi aspetti. Ecco una pura possibilità di Bash. Il trucco è usare in modo execappropriato.

#!/bin/bash

(($#)) || set -- -
while (($#)); do
   { [[ $1 = - ]] || exec < "$1"; } &&
   while read -r; do
      printf '%s\n' "$REPLY"
   done
   shift
done

Il nome file è disponibile in $1.

Se non viene fornito alcun argomento, impostiamo artificialmente -il primo parametro posizionale. Quindi eseguiamo il ciclo sui parametri. Se un parametro non lo è -, reindirizziamo l'input standard dal nome file con exec. Se questo reindirizzamento ha esito positivo, eseguiamo il loop con un whileloop. Sto usando la REPLYvariabile standard e in questo caso non è necessario reimpostare IFS. Se vuoi un altro nome, devi reimpostarlo in questo IFSmodo (a meno che, ovviamente, non lo desideri e sappia cosa stai facendo):

while IFS= read -r line; do
    printf '%s\n' "$line"
done

2

Più accuratamente...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file

2
Presumo che questo sia essenzialmente un commento su stackoverflow.com/a/6980232/45375 , non una risposta. Per rendere esplicito il commento: l'aggiunta IFS=e -r il readcomando assicurano che ogni riga venga letta non modificata (incluso lo spazio bianco iniziale e finale).
mklement0,

2

Si prega di provare il seguente codice:

while IFS= read -r line; do
    echo "$line"
done < file

1
Si noti che anche se modificato, questo non leggerà dall'input standard o da più file, quindi non è una risposta completa alla domanda. (È anche sorprendente vedere due modifiche in pochi minuti più di 3 anni dopo che la risposta è stata presentata per la prima volta.)
Jonathan Leffler

@JonathanLeffler mi dispiace per aver modificato una risposta così vecchia (e non proprio buona) ... ma non potrei sopportare di vedere questo povero readsenza IFS=e -r, e il povero $linesenza le sue citazioni sane.
gniourf_gniourf

1
@gniourf_gniourf: non mi piace la read -rnotazione. IMO, POSIX ha sbagliato; l'opzione dovrebbe abilitare il significato speciale per i backslash finali, non disabilitarlo - in modo che gli script esistenti (prima dell'esistenza di POSIX) non si interrompessero perché -romessi. Osservo, tuttavia, che faceva parte di IEEE 1003.2 1992, che era la prima versione della shell POSIX e degli standard di utility, ma è stata contrassegnata come aggiunta anche allora, quindi si tratta di cianfrusaglie per opportunità ormai lontane. Non ho mai avuto problemi perché il mio codice non usa -r; Devo essere fortunato Ignorami su questo.
Jonathan Leffler

1
@JonathanLeffler Sono davvero d'accordo che -rdovrebbe essere standard. Sono d'accordo che è improbabile che si verifichi nei casi in cui il suo mancato utilizzo comporti problemi. Tuttavia, il codice non funzionante è un codice non funzionante. La mia modifica è stata innescata per la prima volta da quella povera $linevariabile che ha perso le sue virgolette. Ho risolto readmentre ero lì. Non ho risolto il problema echoperché è il tipo di modifica che viene ripristinata. :(.
gniourf_gniourf

1

Il codice ${1:-/dev/stdin}capirà solo il primo argomento, quindi, che ne dici di questo.

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done

1

Non trovo accettabile nessuna di queste risposte. In particolare, la risposta accettata gestisce solo il primo parametro della riga di comando e ignora il resto. Il programma Perl che sta tentando di emulare gestisce tutti i parametri della riga di comando. Quindi la risposta accettata non risponde nemmeno alla domanda. Altre risposte usano estensioni bash, aggiungono comandi "cat" non necessari, funzionano solo per il semplice caso di eco dell'input all'output o sono semplicemente inutilmente complicate.

Tuttavia, devo dare loro un po 'di credito perché mi hanno dato alcune idee. Ecco la risposta completa:

#!/bin/sh

if [ $# = 0 ]
then
        DEFAULT_INPUT_FILE=/dev/stdin
else
        DEFAULT_INPUT_FILE=
fi

# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done

1

Ho combinato tutte le risposte sopra e ho creato una funzione shell adatta alle mie esigenze. Questo proviene da un terminale Cygwin delle mie 2 macchine Windows 10 in cui avevo una cartella condivisa tra di loro. Devo essere in grado di gestire quanto segue:

  • cat file.cpp | tx
  • tx < file.cpp
  • tx file.cpp

Laddove viene specificato un nome file specifico, è necessario utilizzare lo stesso nome file durante la copia. Laddove il flusso di dati di input è stato reindirizzato, è necessario generare un nome file temporaneo con l'ora, i minuti e i secondi. La cartella principale condivisa ha sottocartelle dei giorni della settimana. Questo è per scopi organizzativi.

Ecco, la sceneggiatura definitiva per le mie esigenze:

tx ()
{
  if [ $# -eq 0 ]; then
    local TMP=/tmp/tx.$(date +'%H%M%S')
    while IFS= read -r line; do
        echo "$line"
    done < /dev/stdin > $TMP
    cp $TMP //$OTHER/stargate/$(date +'%a')/
    rm -f $TMP
  else
    [ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
  fi
}

Se c'è un modo che puoi vedere per ottimizzare ulteriormente questo, vorrei sapere.


0

Il seguente funziona con lo standard sh(testato con dashsu Debian) ed è abbastanza leggibile, ma è una questione di gusti:

if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

Dettagli: se il primo parametro non è vuoto, allora catquel file, altrimenti catinput standard. Quindi l'output dell'intera ifistruzione viene elaborato dal commands_and_transformations.


IMHO la migliore risposta così perché indica la vera soluzione: cat "${1:--}" | any_command. La lettura delle variabili shell e l'eco delle stesse possono funzionare per file di piccole dimensioni ma non si adattano così bene.
Andreas Spindler,

Il [ -n "$1" ]può essere semplificato a [ "$1" ].
agc,

0

Questo è facile da usare sul terminale:

$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3

-1

Che ne dite di

for line in `cat`; do
    something($line);
done

L'output di catverrà inserito nella riga di comando. La riga di comando ha una dimensione massima. Inoltre, questo non leggerà riga per riga, ma parola per parola.
Notinlist
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.