Lendo a saída de um comando em uma matriz no Bash

111

Preciso ler a saída de um comando em meu script em uma matriz. O comando é, por exemplo:

ps aux | grep | grep | x 

e dá a saída linha por linha assim:

10
20
30

Preciso ler os valores da saída do comando em uma matriz e, em seguida, farei algum trabalho se o tamanho da matriz for menor que três.

barp
fonte
5
Ei @barp, RESPONDA ÀS SUAS PERGUNTAS, para que seu tipo não esgote a comunidade inteira.
James
9
@James, o problema não é o fato de ele não estar respondendo à sua pergunta ... este é um site de perguntas e respostas. Ele apenas não as marcou como respondidas. Ele deve marcá-los. Dica @ barp
DDPWNAGE
4
Por favor, @barp, marque a pergunta como respondida.
smonff
Relacionado: Looping pelo conteúdo de um arquivo no Bash desde a leitura da saída de um comando por meio da substituição do processo é semelhante à leitura de um arquivo.
codeforester

Respostas:

161

As outras respostas vai quebrar se a saída de comando contém espaços (que é bastante frequente) ou glob personagens como *, ?, [...].

Para obter a saída de um comando em uma matriz, com uma linha por elemento, existem essencialmente três maneiras:

  1. Com Bash≥4 uso mapfile- é o mais eficiente:

    mapfile -t my_array < <( my_command )
  2. Caso contrário, um loop lendo a saída (mais lento, mas seguro):

    my_array=()
    while IFS= read -r line; do
        my_array+=( "$line" )
    done < <( my_command )
  3. Conforme sugerido por Charles Duffy nos comentários (obrigado!), O seguinte pode funcionar melhor do que o método de loop no número 2:

    IFS=$'\n' read -r -d '' -a my_array < <( my_command && printf '\0' )

    Certifique-se de usar exatamente este formulário, ou seja, certifique-se de ter o seguinte:

    • IFS=$'\n' na mesma linha da readinstrução: isso só definirá a variável de ambiente apenas IFS para a readinstrução. Portanto, isso não afetará o resto do seu script. O objetivo desta variável é informar readpara interromper o fluxo no caractere EOL \n.
    • -r: Isso é importante. Diz read para não interpretar as barras invertidas como sequências de escape.
    • -d '': observe o espaço entre a -dopção e seu argumento ''. Se você não deixar um espaço aqui, o ''nunca será visto, pois desaparecerá na etapa de remoção da citação quando o Bash analisar a instrução. Isso indica readpara parar de ler no byte nulo. Algumas pessoas escrevem como -d $'\0', mas não é realmente necessário. -d ''é melhor.
    • -a my_arraydiz readpara preencher a matriz my_arrayenquanto lê o fluxo.
    • Você deve usar a printf '\0'instrução depois my_command , para que readretorne 0; na verdade, não é grande coisa se você não fizer isso (você apenas obterá um código de retorno 1, o que está certo se você não usar set -e- o que você não deveria usar de qualquer maneira), mas apenas tenha isso em mente. É mais limpo e semanticamente correto. Observe que isso é diferente de printf '', que não produz nada. printf '\0'imprime um byte nulo, necessário readpara parar de ler ali (lembra da -d ''opção?).

Se você puder, ou seja, se você tiver certeza de que seu código será executado em Bash≥4, use o primeiro método. E você pode ver que é mais curto também.

Se você quiser usar read, o loop (método 2) pode ter uma vantagem sobre o método 3 se você quiser fazer algum processamento à medida que as linhas são lidas: você tem acesso direto a ele (por meio da $linevariável no exemplo que dei), e você também tem acesso às linhas já lidas (por meio da matriz ${my_array[@]}no exemplo que dei).

Observe que mapfilefornece uma maneira de ter um retorno de chamada avaliado em cada linha lida e, na verdade, você pode até mesmo dizer para ele apenas chamar esse retorno de chamada a cada N linhas lidas; dê uma olhada nas help mapfileopções -Ce -cnele contidas. (Minha opinião sobre isso é que é um pouco desajeitado, mas pode ser usado às vezes se você tiver apenas coisas simples para fazer - eu realmente não entendo por que isso foi implementado em primeiro lugar!).


Agora vou dizer por que o seguinte método:

my_array=( $( my_command) )

está quebrado quando há espaços:

$ # I'm using this command to test:
$ echo "one two"; echo "three four"
one two
three four
$ # Now I'm going to use the broken method:
$ my_array=( $( echo "one two"; echo "three four" ) )
$ declare -p my_array
declare -a my_array='([0]="one" [1]="two" [2]="three" [3]="four")'
$ # As you can see, the fields are not the lines
$
$ # Now look at the correct method:
$ mapfile -t my_array < <(echo "one two"; echo "three four")
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # Good!

Então, algumas pessoas irão recomendar o uso IFS=$'\n'para consertá-lo:

$ IFS=$'\n'
$ my_array=( $(echo "one two"; echo "three four") )
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # It works!

Mas agora vamos usar outro comando, com globs :

$ echo "* one two"; echo "[three four]"
* one two
[three four]
$ IFS=$'\n'
$ my_array=( $(echo "* one two"; echo "[three four]") )
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="t")'
$ # What?

Isso é porque eu tenho um arquivo chamado tno diretório atual ... e esse nome de arquivo é correspondido pelo glob [three four] ... neste ponto, algumas pessoas recomendariam usar set -fpara desabilitar o globbing: mas olhe só: você tem que alterar IFSe usar set -fpara corrigir um técnica quebrada (e você nem mesmo está consertando)! ao fazer isso, estamos realmente lutando contra o shell, não trabalhando com o shell .

$ mapfile -t my_array < <( echo "* one two"; echo "[three four]")
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="[three four]")'

aqui estamos trabalhando com o shell!

gniourf_gniourf
fonte
4
Isso é ótimo, eu nunca ouvi falar mapfileantes, é exatamente o que eu estava perdendo há anos. Eu acho que as versões recentes do bashtem tantos novos recursos interessantes, eu deveria apenas passar alguns dias lendo os documentos e escrevendo um bom cheatsheet.
Gene Pavlovsky de
6
A propósito, para usar esta sintaxe < <(command)em scripts de shell, a linha shebang deve ser #!/bin/bash- se executado como #!/bin/sh, o bash sairá com um erro de sintaxe.
Gene Pavlovsky de
1
Expandindo a observação útil de @ GenePavlovsky, o script também deve ser executado com o comando bash bash my_script.she não com o comando shsh my_script.sh
Vito
2
@Vito: na verdade, esta resposta é apenas para Bash, mas isso não deve ser um problema, já que shells POSIX estritamente compatíveis nem mesmo implementam arrays ( she dashnão sabem nada sobre arrays, exceto, é claro, para a $@matriz de parâmetros posicionais ).
gniourf_gniourf
3
Como outra alternativa que não requer o bash 4.0, considere IFS=$'\n' read -r -d '' -a my_array < <(my_command && printf '\0')- ele funciona corretamente no bash 3.x, e também passa por um status de saída com falha de my_commandpara o read.
Charles Duffy
86

Você pode usar

my_array=( $(<command>) )

para armazenar a saída do comando <command>na matriz my_array.

Você pode acessar o comprimento dessa matriz usando

my_array_length=${#my_array[@]}

Agora o comprimento está armazenado em my_array_length.

Michael Schlottke-Lakemper
fonte
19
E se a saída de $ (comando) tiver espaços e várias linhas com espaços? Eu adicionei "$ (comando)" e ele coloca todas as saídas de todas as linhas no primeiro [0] elemento da matriz.
ikwyl6
3
@ ikwyl6 uma solução alternativa é atribuir a saída do comando a uma variável e, em seguida, fazer uma matriz com ela ou adicioná-la a uma matriz. VAR="$(<command>)"e então my_array=("$VAR")oumy_array+=("$VAR")
Vito
10

Imagine que você vai colocar os arquivos e nomes de diretório (na pasta atual) em um array e contar seus itens. O script seria assim;

my_array=( `ls` )
my_array_length=${#my_array[@]}
echo $my_array_length

Ou você pode iterar sobre essa matriz adicionando o seguinte script:

for element in "${my_array[@]}"
do
   echo "${element}"
done

Observe que este é o conceito central e a entrada é considerada como sanitizada antes, ou seja, remover caracteres extras, manusear Strings vazias e etc. (o que está fora do tópico deste tópico).

Youness
fonte
3
Ideia terrível pelos motivos mencionados na resposta acima
Hubert Grzeskowiak