Como separar a saída do comando para linhas individuais

8
list='ls -a R*'
echo $list

Dentro de um script de shell, esse 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 que acontecem com ls , du , find -type -d , etc.

    
por Anony 02.11.2017 / 12:18

4 respostas

9

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ços em branco (espaços, novas linhas, tabulações) e esses serão perdidos.

    
por muru 02.11.2017 / 12:24
13

Em vez de colocar a saída ls em uma variável e, em seguida, echo ing, 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 você a fazer nada com a saída de ls , exceto exibi-lo:)

Use, por exemplo, shell globs e um for loop para fazer algo com arquivos ...

shopt -s dotglob                  # to include hidden files*

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

* isso não incluirá o diretório atual . ou seu pai .. embora

    
por Zanna 02.11.2017 / 12:36
9

Embora colocar entre aspas como @muru sugerido realmente faça o que você pediu, você também pode querer considerar usando uma matriz para isso. Por exemplo:

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

O IFS=$'\n' 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, então a file name with spaces.txt seria 5 elementos separados em vez de um. Essa abordagem será interrompida se seus 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 também alterei o estilo antigo 'command' para $(command) , que é a sintaxe preferida.

Agora você tem uma matriz chamada $dirs , cada bof cujos elementos é 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 (consulte aqui ), você precisará redefinir IFS de volta para o valor original depois de fazer isso. Então, salve e reasign:

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

Ou faça isso manualmente:

IFS=" "$'\t\n '

Alternativamente, basta fechar o terminal atual. Seu novo terá o IFS original definido novamente.

    
por terdon 02.11.2017 / 12:38
0

Se você precisar armazenar a saída e desejar uma matriz, mapfile facilitará o processo.

Primeiro, considere se você precisa para armazenar a saída do seu comando. Se você não fizer isso, 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 globbing, definir IFS para dividir em linhas, usar substituição de comando dentro de ( ) sintaxe de criação da matriz e redefinir IFS depois, que é mais complexo do que parece. a resposta de terdon cobre algumas dessas abordagens. Mas eu sugiro que você use mapfile , o embutido do shell Bash para ler texto como uma matriz de linhas, em vez disso. Aqui está um caso simples em que você está lendo de um arquivo:

mapfile < filename

Isso lê as linhas em uma matriz chamada MAPFILE . Sem o redirecionamento de entrada < filename , você estaria lendo do shell < a href="https://en.wikipedia.org/wiki/Standard_streams#Standard_input_.28stdin.29"> entrada padrão (normalmente seu terminal) em vez do arquivo chamado filename . Para ler em uma matriz diferente do padrão MAPFILE , passe seu nome. Por exemplo, isso lê na matriz lines :

mapfile lines < filename

Outro comportamento padrão que você pode optar por mudar é que os caracteres da 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 tenha terminado sem um caractere de nova linha, caso em que o último elemento não possui um). Para chupar essas novas linhas para que elas não apareçam na matriz, passe a opção -t para mapfile . Por exemplo, isso lê a matriz records e não grava caracteres de nova linha à direita em seus elementos de matriz:

mapfile -t records < filename

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

O shell mapfile incorporado suporta outras opções e, como alternativa, pode ser chamado como readarray . Execute help mapfile (e help readarray ) para detalhes.

Mas você não quer ler de um arquivo, você quer ler a saída de um comando. Para conseguir isso, use substituição de processos . Este comando lê as linhas do comando some-command com arguments... como seus argumentos de linha de comando e coloca nelas na matriz padrão mapfile MAPFILE , com seus caracteres de nova linha de terminação removidos:

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

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

Você pode pensar que poderia renunciar à substituição do processo usando some-command arguments... | mapfile -t , mas isso não funcionará porque, quando você tiver um pipeline de vários comandos separados por | , Bash executado executa todos os comandos em subshells . Assim, tanto some-command arguments... como mapfile -t são executados em seus próprios ambientes , inicializado, mas separado do ambiente do shell no qual você executa o pipeline. No subshell onde mapfile -t é executado, o MAPFILE array é preenchido, mas, em seguida, esse array é descartado quando o comando é finalizado. MAPFILE nunca é criado ou modificado para o chamador.

Veja como o exemplo na resposta de terdon se parece, na sua totalidade, se você usar mapfile :

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

É isso. Você não precisa verifique se IFS está definido , seu valor, defina-o como uma nova linha e redefina ou desmarque-o mais tarde. Você não precisa desativar globbing (por exemplo, com set -f ) - que é realmente necessário se você quiser usar esse método seriamente, pois os nomes de arquivos podem conter * , ? e [ - depois reative-o ( set +f ) depois.

Você também pode substituir esse loop inteiro inteiramente por um único comando printf - embora isso não seja realmente um benefício de mapfile , como você pode usar se usar mapfile ou 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 terdon menciona , a operação linha a linha nem sempre é apropriada, e não funcionará corretamente com nomes de arquivos que contenham novas linhas. Eu recomendo contra nomear arquivos dessa maneira, mas 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 mapfile aproxima-se desse objetivo, mas peço que reconsidere seus requisitos.

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

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

Você também pode usar um comando externo como sed para adicionar DIR: ao início de cada linha. Isto é discutivelmente um pouco feio, e ao contrário do comando find ele adicionará "prefixos" extras dentro de nomes de arquivos que contêm novas linhas, mas funciona independentemente de sua entrada para que ele atenda seus requisitos e ainda seja preferível ler o saída em uma 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-command e passar o caminho do diretório como um argumento, find permite que você faça isso também:

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

Como outro exemplo, vamos retornar à tarefa geral de adicionar um prefixo a cada linha. Suponha que eu queira ver a saída de help mapfile mas número de linhas hte. Eu realmente não usaria mapfile para isso, nem qualquer outro método que o lê em uma variável de shell ou matriz de shell. Supondo que help mapfile | cat -n não forneça a formatação que eu quero, posso usar awk :

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

Ler toda a saída de um comando em uma única variável ou matriz às vezes é útil e apropriado, mas tem grandes desvantagens. Você não precisa apenas lidar com a complexidade adicional de usar sua shell quando um comando existente ou combinação de comandos existentes já pode fazer o que você precisa tão bem ou melhor, mas toda a saída do comando tem que 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 que é comumente tentada - e às vezes feita corretamente - é ler a entrada linha por linha com read -r em um loop. Se você não precisa armazenar linhas anteriores enquanto opera em linhas posteriores, e você tem que usar uma entrada longa, então 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 .

    
por Eliah Kagan 04.11.2017 / 12:25

Tags