Como separar a saída do comando em linhas individuais

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

Dentro de um script de shell, este comando echo listará todos os arquivos do diretório atual começando com R, mas em uma linha. Como posso imprimir cada item em uma linha?

Eu preciso de um comando genérico para todos os cenários acontecendo com ls, du, find -type -d, etc.

Anony
fonte
6
Nota geral: não faça isso com ls, pois ele quebrará qualquer nome de arquivo estranho . Veja aqui .
terdon

Respostas:

12

Se a saída do comando contiver várias linhas, cite suas variáveis ​​para preservar essas novas linhas ao ecoar:

echo "$list"

Caso contrário, o shell expandirá e dividirá o conteúdo da variável em espaço em branco (espaços, novas linhas, guias) e esses serão perdidos.

muru
fonte
15

Em vez de colocar uma lssaída imprudentemente em uma variável e depois echoinseri-la, o que remove todas as cores, use

ls -a1

De man ls

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

Eu não aconselho que você faça nada com a saída de ls, exceto exibi-lo :)

Use, por exemplo, globs de shell e um forloop para fazer algo com arquivos ...

shopt -s dotglob                  # to include hidden files*

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

* isso não inclui o diretório atual .ou seu pai, ..embora

Zanna
fonte
1
Eu recomendaria substituir: echo "$i"por printf "%s\n" "$i" (aquele não bloqueará se um nome de arquivo começar com um "-"). Na verdade, na maioria das vezes echo somethingdeve ser substituído por printf "%s\n" "something".
Olivier Dulac
2
@OlivierDulac Todos começam Raqui, mas em geral sim. No entanto, mesmo para scripts, tentar sempre acomodar a liderança -nos caminhos causa problemas: as pessoas assumem --, que apenas alguns comandos suportam, significa o fim das opções. Eu vi find . [tests] -exec [cmd] -- {} \;onde [cmd]não suporta --. Todo caminho começa com .qualquer maneira! Mas isso não é argumento contra a substituição echocom printf '%s\n'. Aqui, pode-se substituir o loop inteiro por printf '%s\n' R/*. O Bash tem printfcomo interno, portanto, não há efetivamente nenhum limite para o número / comprimento dos argumentos.
Eliah Kagan 3/11
12

Embora colocá-lo entre aspas, como o @muru sugeriu , de fato fará o que você pediu, você também pode considerar o uso de uma matriz para isso. Por exemplo:

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

O IFS=$'\n'comando diz ao bash para dividir apenas a saída em caracteres de nova linha para obter cada elemento da matriz. Sem ele, ele será dividido em espaços, portanto, a file name with spaces.txtseriam 5 elementos separados em vez de um. Essa abordagem será interrompida se os nomes de arquivos / diretórios puderem conter novas linhas ( \n). Ele salvará cada linha da saída do comando como um elemento da matriz.

Observe que eu também mudei o estilo antigo `command`para o $(command)qual é a sintaxe preferida.

Agora você tem uma matriz chamada $dirs, cada bof cujos elementos são uma linha da saída do comando anterior. Por exemplo:

$ 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

Agora, devido a alguma estranheza do bash (veja aqui ), você precisará redefinir IFSo valor original depois de fazer isso. Portanto, salve-o e reasigne:

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

Ou faça isso manualmente:

IFS=" "$'\t\n '

Como alternativa, basta fechar o terminal atual. O seu novo terá o IFS original definido novamente.

Terdon
fonte
Eu poderia ter sugerido algo parecido com isto, mas a parte sobre dufaz parecer OP está mais interessada em preservar o formato de saída.
Muru
Como posso fazer isso funcionar se os nomes de diretório contiverem espaços?
Dessert
@dessert whoops! Ele deve funcionar com espaços (mas não com novas linhas) agora. Obrigado por apontar isso.
terdon
1
Observe que você deve redefinir ou desabilitar o IFS após a atribuição da matriz. unix.stackexchange.com/q/264635/70524
muru
@ muru oh uau, obrigado, eu tinha esquecido essa estranheza.
Terdon
2

Se você precisar armazenar a saída e desejar uma matriz, mapfileserá mais fácil.

Primeiro, considere se você precisa armazenar a saída do seu comando. Caso contrário, basta executar o comando

Se você decidir ler a saída de um comando como uma matriz de linhas, é verdade que uma maneira de fazê-lo é desativar o globbing, definir IFSa divisão em linhas, usar a substituição de comando dentro da ( )sintaxe de criação da matriz e redefinir IFSdepois, o que é mais complexo do que parece. A resposta de Terdon cobre parte dessa abordagem. Mas eu sugiro que você use mapfile, o shell Bash é built-in comando para leitura de texto como um conjunto de linhas, em vez disso. Aqui está um caso simples em que você está lendo de um arquivo:

mapfile < filename

Isso lê linhas em uma matriz chamada MAPFILE. Sem o redirecionamento de entrada , você estaria lendo a entrada padrão do shell (normalmente o seu terminal) em vez do arquivo nomeado por . Para ler em uma matriz que não seja o padrão , passe seu nome. Por exemplo, isso lê na matriz :< filenamefilenameMAPFILElines

mapfile lines < filename

Outro comportamento padrão que você pode optar por alterar é que os caracteres de nova linha no final das linhas são deixados no lugar; eles aparecem como o último caractere em cada elemento da matriz (a menos que a entrada termine sem um caractere de nova linha; nesse caso, o último elemento não possui um). Para incluir essas novas linhas de forma que elas não apareçam na matriz, passe a -topção para mapfile. Por exemplo, isso é lido na matriz recordse não grava caracteres de nova linha à direita em seus elementos da matriz:

mapfile -t records < filename

Você também pode usar -tsem passar um nome de matriz; isto é, ele também funciona com um nome implícito de matriz MAPFILE.

O mapfileshell embutido suporta outras opções e, alternativamente, pode ser chamado como readarray. Execute help mapfile(e help readarray) para obter detalhes.

Mas você não deseja ler de um arquivo, mas da saída de um comando. Para conseguir isso, use a substituição do processo . Este comando lê as linhas do comando some-commandcom arguments...seus argumentos de linha de comando e as coloca na mapfilematriz padrão MAPFILE, com seus caracteres finais de nova linha removidos:

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

A substituição do processo substitui por um nome de arquivo real a partir do qual a saída da execução pode ser lida. O arquivo é um pipe nomeado em vez de um arquivo normal e, no Ubuntu, será nomeado como (às vezes com outro número ), mas você não precisa se preocupar com os detalhes, porque o shell cuida disso tudo nos bastidores.<(some-command arguments...)some-command arguments.../dev/fd/6363

Você pode pensar que poderia renunciar à substituição do processo usando , mas isso não funcionará porque, quando você tem um pipeline de vários comandos separados por , o Bash executa todos os comandos em subshells . Assim, tanto e executar em seus próprios ambientes , inicializadas a partir, mas separado do ambiente do shell em que você executar o pipeline. No subshell onde é executado, a matriz é preenchida, mas essa matriz é descartada quando o comando termina. nunca é criado ou modificado para o chamador.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Aqui está a aparência do exemplo na resposta de terdon , na íntegra, se você usar mapfile:

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

É isso aí. Você não precisa verificar se IFSfoi definido , acompanhar se não foi definido e com qual valor, defini-lo como uma nova linha e redefini-lo ou reconfigurá-lo posteriormente. Você não precisa desativar o globbing (por exemplo, com set -f) - o que é realmente necessário para usar esse método com seriedade, pois os nomes de arquivos podem conter *e ?- e [depois reativá-lo ( set +f) posteriormente.

Você também pode substituir esse loop específico inteiramente por um único printfcomando - embora isso não seja realmente um benefício mapfile, pois você pode fazer isso usando mapfileou outro método para preencher a matriz. Aqui está uma versão mais curta:

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

É importante ter em mente que, como menciona Terdon , operar linha por linha nem sempre é apropriado e não funcionará corretamente com nomes de arquivos que contêm novas linhas. Eu recomendo nomear arquivos dessa maneira, mas isso pode acontecer, inclusive por acidente.

Não existe realmente uma solução única para todos.

Você pediu "um comando genérico para todos os cenários" e a abordagem de usar mapfileum pouco aproxima esse objetivo, mas peço que você reconsidere seus requisitos.

A tarefa mostrada acima é melhor alcançada com apenas um único findcomando:

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

Você também pode usar um comando externo como sedadicionar DIR: ao início de cada linha. É indiscutivelmente um pouco feio e, ao contrário do comando find, ele adicionará "prefixos" extras nos nomes de arquivos que contêm novas linhas, mas funciona independentemente da entrada, portanto atende aos seus requisitos e ainda é preferível ler a saída em um arquivo. variável ou matriz :

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

Se você precisar listar e também executar uma ação em cada diretório encontrado, como executar some-commande passar o caminho do diretório como argumento, findfaça isso também:

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

Como outro exemplo, voltemos à tarefa geral de adicionar um prefixo a cada linha. Suponha que eu queira ver a saída de, help mapfilemas numere as linhas. Na verdade, eu não usaria mapfileisso nem qualquer outro método que o leia em uma variável de shell ou matriz de shell. Supondo help mapfile | cat -nque não dê a formatação que eu quero, eu posso usar awk:

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

Ler toda a saída de um comando em uma única variável ou matriz às vezes é útil e apropriado, mas possui grandes desvantagens. Você não precisa apenas lidar com a complexidade adicional de usar seu shell quando um comando ou combinação de comandos existentes já pode fazer o que você precisa melhor ou melhor, mas toda a saída do comando deve ser armazenada na memória. Às vezes você pode saber que isso não é um problema, mas às vezes você pode estar processando um arquivo grande.

Uma alternativa comumente tentada - e às vezes feita corretamente - é ler a entrada linha por linha read -rem um loop. Se você não precisar armazenar linhas anteriores enquanto estiver operando em linhas posteriores e precisar usar entradas longas, pode ser melhor que mapfile. Mas também deve ser evitado nos casos em que você pode simplesmente canalizá-lo para um comando que pode fazer o trabalho, que é a maioria dos casos .

Eliah Kagan
fonte