Come separare l'output del comando su singole righe


12
list=`ls -a R*`
echo $list

All'interno di uno script shell, questo comando echo elencherà tutti i file dalla directory corrente a partire da R, ma in una riga. Come posso stampare ogni articolo su una riga?

Ho bisogno di un comando generico per tutti gli scenari accadendo con ls, du, find -type -d, etc.


6
Nota generale: non farlo con ls, si spezzerà su qualsiasi strano nome di file . Vedi qui .
terdon,

Risposte:


12

Se l'output del comando contiene più righe, cita le tue variabili per preservare quelle nuove righe durante l'eco:

echo "$list"

Altrimenti, la shell espanderà e dividerà i contenuti variabili su spazi bianchi (spazi, newline, tab) e quelli andranno persi.


15

Invece di sconsiderare di inserire l' lsoutput in una variabile e quindi di echoinserirlo, che rimuove tutti i colori, utilizzare

ls -a1

A partire dal man ls

       -1     list one file per line.  Avoid '\n' with -q or -b

Non ti consiglio di fare nulla con l'output di ls, tranne mostrarlo :)

Usa, ad esempio, shell globs e un forloop per fare qualcosa con i file ...

shopt -s dotglob                  # to include hidden files*

for i in R/*; do echo "$i"; done

* Questo non includerà la directory corrente .o della sua controllante ..però


1
Consiglierei di sostituire: echo "$i"con printf "%s\n" "$i" (quello non si strozzerà se un nome file inizia con un "-"). In realtà, la maggior parte delle volte echo somethingdovrebbe essere sostituita da printf "%s\n" "something".
Olivier Dulac,

2
@OlivierDulac Iniziano tutti da Rqui, ma in generale sì. Tuttavia, anche per gli script, il tentativo di adattarsi sempre ai -percorsi principali causa problemi: le persone presumono --, che solo alcuni comandi supportano, significano la fine delle opzioni. Ho visto find . [tests] -exec [cmd] -- {} \;dove [cmd]non supporta --. Ogni percorso lì inizia .comunque! Ma questo non è un argomento contro la sostituzione echocon printf '%s\n'. Qui, è possibile sostituire l'intero loop con printf '%s\n' R/*. Bash ha printfun built-in, quindi di fatto non c'è limite al numero / lunghezza degli argomenti.
Eliah Kagan,

12

Mettendolo tra virgolette come suggerito da @muru farà davvero quello che hai chiesto, potresti anche considerare di usare un array per questo. Per esempio:

IFS=$'\n' dirs=( $(find . -type d) )

La IFS=$'\n'dice bash dividere solo l'uscita sulla nuova riga characcters o ottenere ogni elemento della matrice. Senza di essa, si dividerà su spazi, quindi a file name with spaces.txtsarebbero 5 elementi separati anziché uno. Questo approccio si interromperà se i nomi dei file / directory possono contenere newline ( \n). Salverà ogni riga dell'output del comando come elemento dell'array.

Si noti che ho anche cambiato il vecchio stile `command`in $(command)cui è la sintassi preferita.

Ora hai un array chiamato $dirs, ogni bof i cui elementi è una riga dell'output del comando precedente. Per esempio:

$ find . -type d
.
./olad
./ho
./ha
./ads
./bar
./ga
./da
$ IFS=$'\n' dirs=( $(find . -type d) )
$ for d in "${dirs[@]}"; do
    echo "DIR: $d"
  done
DIR: .
DIR: ./olad
DIR: ./ho
DIR: ./ha
DIR: ./ads
DIR: ./bar
DIR: ./ga
DIR: ./da

Ora, a causa di una certa stranezza bash (vedi qui ), dovrai ripristinare IFSil valore originale dopo averlo fatto. Quindi, salvalo e riassegna:

oldIFS="$IFS"
IFS=$'\n' dirs=( $(find . -type d) ) 
IFS="$oldIFS"

Oppure fallo manualmente:

IFS=" "$'\t\n '

In alternativa, basta chiudere il terminale corrente. Il tuo nuovo avrà di nuovo impostato l'IFS originale.


Avrei potuto suggerire qualcosa del genere, ma poi la parte su ducome appare OP è più interessata a preservare il formato di output.
muru,

Come posso farlo funzionare se i nomi delle directory contengono spazi?
dessert,

@dessert whoops! Ora dovrebbe funzionare con gli spazi (ma non con le nuove linee). Grazie per segnalarlo.
terdon,

1
Si noti che è necessario ripristinare o annullare l'IFS dopo tale assegnazione di array. unix.stackexchange.com/q/264635/70524
muru

@muru oh wow, grazie, mi ero dimenticato di quella stranezza.
terdon,

2

Se è necessario archiviare l'output e si desidera un array, mapfileè semplice.

Innanzitutto, considera se è necessario memorizzare l'output del comando. In caso contrario, basta eseguire il comando.

Se decidi di voler leggere l'output di un comando come un array di righe, è vero che un modo per farlo è disabilitare il globbing, impostare la IFSdivisione su righe, utilizzare la sostituzione dei comandi all'interno della ( )sintassi di creazione dell'array e reimpostare in IFSseguito, che è più complesso di quanto sembri. La risposta di Terdon copre parte di tale approccio. Ma ti suggerisco di usare mapfile, invece, il comando integrato della shell Bash per leggere il testo come una matrice di righe. Ecco un semplice caso in cui stai leggendo da un file:

mapfile < filename

Questo legge le righe in un array chiamato MAPFILE. Senza il reindirizzamento dell'input , dovresti leggere dall'input standard della shell (in genere il tuo terminale) invece del file indicato da . Per leggere in un array diverso da quello predefinito , passane il nome. Ad esempio, questo si legge nell'array :< filenamefilenameMAPFILElines

mapfile lines < filename

Un altro comportamento predefinito che potresti scegliere di modificare è che i caratteri di nuova riga alla fine delle righe siano lasciati al loro posto; appaiono come l'ultimo carattere in ciascun elemento dell'array (a meno che l'input non sia terminato senza un carattere di nuova riga, nel qual caso l'ultimo elemento non ne ha uno). Per tagliare queste nuove righe in modo che non appaiano nella matrice, passare l' -topzione a mapfile. Ad esempio, questo legge l'array recordse non scrive i caratteri di nuova riga finali nei suoi elementi dell'array:

mapfile -t records < filename

È inoltre possibile utilizzare -tsenza passare un nome di array; vale a dire, funziona anche con un nome di matrice implicito di MAPFILE.

La mapfileshell incorporata supporta altre opzioni e può essere invocata come readarray. Esegui help mapfile(e help readarray) per i dettagli.

Ma non vuoi leggere da un file, vuoi leggere dall'output di un comando. A tale scopo, utilizzare la sostituzione del processo . Questo comando legge le linee dal comando some-commandcon arguments...come i suoi argomenti della riga di comando e luoghi a loro a mapfile's array predefinito MAPFILE, con i loro personaggi di fine riga rimossi:

mapfile -t < <(some-command arguments...)

La sostituzione del processo sostituisce con un nome file effettivo da cui è possibile leggere l'output dell'esecuzione . Il file è una pipe denominata piuttosto che un normale file e, su Ubuntu, sarà chiamato come (a volte con un numero diverso da quello ), ma non è necessario preoccuparsi dei dettagli, perché la shell si prende cura di esso tutto dietro le quinte.<(some-command arguments...)some-command arguments.../dev/fd/6363

Potresti pensare di poter rinunciare alla sostituzione del processo usando invece, ma ciò non funzionerà perché, quando hai una pipeline di più comandi separati da , Bash Run esegue tutti i comandi in subshells . Così sia e correre nei loro ambienti , inizializzati da separare, ma dall'ambiente del guscio in cui si esegue la pipeline. Nel subshell in cui corre, la serie fa vengono popolate, ma poi tale matrice viene scartato quando finisce il comando. non viene mai creato o modificato per il chiamante.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Ecco come appare l'esempio nella risposta di terdon , nel suo complesso, se si utilizza mapfile:

mapfile -t dirs < <(find . -type d)
for d in "${dirs[@]}"; do
    echo "DIR: $d"
done

Questo è tutto. Non è necessario verificare se è IFSstato impostato , tenere traccia di se non è stato impostato e con quale valore, impostarlo su una nuova riga, quindi reimpostare o reinserirlo in un secondo momento. Non è necessario disabilitare globbing (per esempio, con set -f) - che è realmente necessario se si è di utilizzare quel metodo serio, dal momento che i nomi dei file possono contenere *, ?e [--quindi riattivarla ( set +f) in seguito.

Puoi anche sostituire quel particolare ciclo interamente con un singolo printfcomando, anche se questo non è in realtà un vantaggio mapfile, in quanto puoi farlo se usi mapfileo un altro metodo per popolare l'array. Ecco una versione più breve:

mapfile -t < <(find . -type d)
printf 'DIR: %s\n' "${MAPFILE[@]}"

È importante tenere presente che, come indicato da Terdon , operare riga per riga non è sempre appropriato e non funzionerà correttamente con nomi di file che contengono nuove righe. Consiglio di non nominare i file in questo modo, ma può succedere, anche per caso.

Non esiste davvero una soluzione unica per tutti.

Hai chiesto "un comando generico per tutti gli scenari" e l'approccio all'utilizzo in mapfilequalche modo di tali obiettivi, ma ti esorto a riconsiderare le tue esigenze.

Il compito mostrato sopra è meglio realizzato con un solo findcomando:

find . -type d -printf 'DIR: %p\n'

È inoltre possibile utilizzare un comando esterno come sedaggiungere DIR: all'inizio di ogni riga. Questo è probabilmente un po 'brutto e, a differenza di quel comando find, aggiungerà "prefissi" extra all'interno di nomi di file che contengono nuove righe, ma funziona indipendentemente dal suo input, quindi soddisfa i tuoi requisiti ed è comunque preferibile leggere l'output in un variabile o matrice :

find . -type d | sed 's/^/DIR: /'

Se è necessario elencare ed eseguire anche un'azione su ciascuna directory trovata, come eseguire some-commande passare il percorso della directory come argomento, è findpossibile farlo anche:

find . -type d -print -exec some-command {} \;

Come altro esempio, torniamo all'attività generale di aggiungere un prefisso a ciascuna riga. Supponiamo che io voglia vedere l'output di help mapfilema numerare le righe. In realtà non userei mapfileper questo, né qualsiasi altro metodo che lo legga in una variabile shell o array di shell. Supponendo help mapfile | cat -nche non dia la formattazione che desidero, posso usare awk:

help mapfile | awk '{ printf "%3d: %s\n", NR, $0 }'

Leggere tutto l'output di un comando in una singola variabile o matrice è talvolta utile e appropriato, ma presenta importanti svantaggi. Non solo devi affrontare l'ulteriore complessità dell'uso della shell quando un comando esistente o una combinazione di comandi esistenti può già fare ciò che ti serve o meglio, ma l'intero output del comando deve essere archiviato in memoria. A volte potresti sapere che non è un problema, ma a volte potresti elaborare un file di grandi dimensioni.

Un'alternativa che viene comunemente tentata - e talvolta eseguita correttamente - è leggere l'input riga per riga con read -rin un ciclo. Se non è necessario memorizzare le righe precedenti mentre si opera su righe successive e è necessario utilizzare input lunghi, potrebbe essere meglio di mapfile. Ma anche questo dovrebbe essere evitato nei casi in cui puoi semplicemente inviarlo a un comando che può fare il lavoro, che è nella maggior parte dei casi .

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.