Como posso armazenar os resultados do comando “find” como uma matriz no Bash

96

Estou tentando salvar o resultado de findcomo matrizes. Aqui está o meu código:

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=`find . -name ${input}`

len=${#array[*]}
echo "found : ${len}"

i=0

while [ $i -lt $len ]
do
echo ${array[$i]}
let i++
done

Recebo 2 arquivos .txt no diretório atual. Portanto, espero '2' como resultado de ${len}. No entanto, ele imprime 1. O motivo é que leva todos os resultados de findcomo um elemento. Como posso consertar isso?

PS
Eu encontrei várias soluções no StackOverFlow sobre um problema semelhante. No entanto, eles são um pouco diferentes, então não posso aplicar no meu caso. Preciso armazenar os resultados em uma variável antes do loop. Obrigado novamente.

Juneyoung Oh
fonte

Respostas:

137

Atualização 2020 para usuários Linux:

Se você tem uma versão atualizada do bash (4.4-alpha ou melhor), como provavelmente tem se estiver no Linux, então você deve usar a resposta de Benjamin W ..

Se você estiver no Mac OS, que - pelo menos eu verifiquei - ainda usava o bash 3.2 ou, de outra forma, está usando um bash mais antigo, continue na próxima seção.

Resposta para bash 4.3 ou anterior

Aqui está uma solução para obter a saída de findem uma bashmatriz:

array=()
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done < <(find . -name "${input}" -print0)

Isso é complicado porque, em geral, os nomes dos arquivos podem ter espaços, novas linhas e outros caracteres hostis ao script. A única maneira de usar finde ter os nomes dos arquivos separados uns dos outros com segurança é usar o -print0que imprime os nomes dos arquivos separados por um caractere nulo. Isso não seria muito inconveniente se as funções readarray/ do bash mapfilesuportassem strings separadas por nulos, mas não o fazem. O de Bash readsim e isso nos leva ao loop acima.

[Esta resposta foi escrita originalmente em 2014. Se você tiver uma versão recente do bash, consulte a atualização abaixo.]

Como funciona

  1. A primeira linha cria uma matriz vazia: array=()

  2. Cada vez que a readinstrução é executada, um nome de arquivo separado por nulo é lido da entrada padrão. A -ropção diz readpara deixar os caracteres de barra invertida sozinhos. O -d $'\0'informa readque a entrada será separada por nulos. Desde que omitir o nome para read, a concha coloca a entrada para o nome padrão: REPLY.

  3. A array+=("$REPLY")instrução anexa o novo nome do arquivo ao array array.

  4. A linha final combina redirecionamento e substituição de comando para fornecer a saída findpara a entrada padrão do whileloop.

Por que usar a substituição de processo?

Se não usássemos a substituição de processo, o loop poderia ser escrito como:

array=()
find . -name "${input}" -print0 >tmpfile
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done <tmpfile
rm -f tmpfile

Acima, a saída de findé armazenada em um arquivo temporário e esse arquivo é usado como entrada padrão para o loop while. A ideia da substituição do processo é tornar esses arquivos temporários desnecessários. Portanto, em vez de fazer com que o whileloop obtenha seu stdin tmpfile, podemos fazer com que ele obtenha seu stdin de <(find . -name ${input} -print0).

A substituição de processos é amplamente útil. Em muitos lugares onde um comando deseja ler de um arquivo, você pode especificar a substituição de processo,, em <(...)vez de um nome de arquivo. Há uma forma análoga,, >(...)que pode ser usada no lugar do nome do arquivo onde o comando deseja gravar no arquivo.

Como os arrays, a substituição de processos é um recurso do bash e de outros shells avançados. Não faz parte do padrão POSIX.

Alternativa: lastpipe

Se desejado, lastpipepode ser usado em vez de substituição de processo (gorjeta: César ):

set +m
shopt -s lastpipe
array=()
find . -name "${input}" -print0 | while IFS=  read -r -d $'\0'; do array+=("$REPLY"); done; declare -p array

shopt -s lastpipediz ao bash para executar o último comando no pipeline no shell atual (não no fundo). Dessa forma, o arraypermanece após a conclusão do pipeline. Porque lastpipesó tem efeito se o controle de trabalho estiver desativado, nós corremos set +m. (Em um script, ao contrário da linha de comando, o controle de tarefas está desativado por padrão.)

Notas Adicionais

O comando a seguir cria uma variável de shell, não uma matriz de shell:

array=`find . -name "${input}"`

Se você quiser criar uma matriz, precisará colocar parênteses ao redor da saída de find. Então, ingenuamente, pode-se:

array=(`find . -name "${input}"`)  # don't do this

O problema é que o shell executa a divisão de palavras nos resultados de, de findmodo que os elementos do array não são garantidos como sendo o que você deseja.

Atualização 2019

A partir da versão 4.4-alpha, o bash agora suporta uma -dopção para que o loop acima não seja mais necessário. Em vez disso, pode-se usar:

mapfile -d $'\0' array < <(find . -name "${input}" -print0)

Para mais informações sobre este assunto, consulte (e upvote) a resposta de Benjamin W. .

John1024
fonte
1
@JuneyoungOh Que bom que ajudou. Eu adicionei uma seção de substituição de processo.
John1024
3
@Rockallite Essa é uma boa observação, mas incompleta. Embora seja verdade que não nos dividimos em várias palavras, ainda precisamos IFS=evitar a remoção de espaços em branco no início ou no final das linhas de entrada. Você pode testar isso facilmente comparando a saída de read var <<<' abc '; echo ">$var<"com a saída de IFS= read var <<<' abc '; echo ">$var<". No primeiro caso, os espaços antes e depois abcsão removidos. No último, eles não são. Nomes de arquivos que começam ou terminam com espaço em branco podem ser incomuns, mas, se existirem, queremos que sejam processados ​​corretamente.
John1024
1
Olá, depois de executar seu código, recebo um erro de sintaxe de mensagem próximo ao token inesperado <' concluído <<(find aaa / -not -newermt "$ last_build_timestamp_v" -type f -print0) '
Przemysław Sienkiewicz
1
Uma nota: o mais simples ''pode ser usado em vez de $'\0':n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(printf 'first\nstill first\0second\0third')
glenn jackman
1
@theeagle Presumo que você pretendia escrever BLAH=$(find . -name '*.php'). Conforme discutido na resposta, essa abordagem funcionará em casos limitados, mas não funcionará em geral com todos os nomes de arquivo e não produz, como o OP esperava, um array .
John1024
36

O Bash 4.4 introduziu uma -dopção para readarray/ mapfile, então isso agora pode ser resolvido com

readarray -d '' array < <(find . -name "$input" -print0)

para um método que funciona com nomes de arquivos arbitrários, incluindo espaços em branco, novas linhas e caracteres globbing. Isso requer que o seu findsuporte -print0, como por exemplo o GNU find faz.

No manual (omitindo outras opções):

mapfile [-d delim] [array]

-d
O primeiro caractere de delimé usado para encerrar cada linha de entrada, em vez de nova linha. Se delimfor uma string vazia, mapfileterminará uma linha ao ler um caractere NUL.

E readarrayé apenas sinônimo de mapfile.

Benjamin W.
fonte
18

Se você estiver usando bash4 ou posterior, pode substituir o uso de findpor

shopt -s globstar nullglob
array=( **/*"$input"* )

O **padrão ativado por globstarcorresponde a 0 ou mais diretórios, permitindo que o padrão corresponda a uma profundidade arbitrária no diretório atual. Sem a nullglobopção, o padrão (após a expansão do parâmetro) é tratado literalmente, portanto, sem correspondências, você teria uma matriz com uma única string em vez de uma matriz vazia.

Adicione a dotglobopção à primeira linha também se quiser percorrer diretórios ocultos (como .ssh) e combinar arquivos ocultos (como .bashrc) também.

chepner
fonte
4
Talvez nullglobtambém ...
kojiro
1
Sim, sempre me esqueço disso.
Chepner
5
Observe que isso não incluirá os arquivos e diretórios ocultos, a menos que dotglobseja definido (isso pode ou não ser desejado, mas também vale a pena mencionar).
gniourf_gniourf
10

você pode tentar algo como

array=(`find . -type f | sort -r | head -2`)
, e para imprimir os valores do array, você pode tentar algo como echo "${array[*]}"

Ahmed Al-Haffar
fonte
8
Interrompe se houver nomes de arquivo com espaços ou caracteres glob.
gniourf_gniourf
2

O seguinte parece funcionar para Bash e Z Shell no macOS.

#! /bin/sh

IFS=$'\n'
paths=($(find . -name "foo"))
unset IFS

printf "%s\n" "${paths[@]}"
Sunknudsen
fonte
Isso funciona com arquivos com espaços e outros caracteres especiais, falha com o caso (reconhecidamente raro) de arquivos com quebra de linha em seu nome. Você pode criar um para um teste comprintf "%b" "file name with spaces, a star * ...\012and a second line\0" | xargs -0 touch
Stéphane Gourichon
talvez eu esteja faltando alguma coisa aqui, mas esta parece ser a solução muito mais clara e fácil para 99% dos casos
Matt Korostoff
-1

No bash, $(<any_shell_cmd>)ajuda a executar um comando e capturar a saída. Passar isso para IFScom \ncomo delimitador ajuda a convertê-lo em uma matriz.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")
Rashok
fonte
4
Isso obterá apenas o primeiro arquivo dos resultados do findarray.
Benjamin W.
-2

Você poderia fazer assim:

#!/bin/bash
echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=(`find . -name '*'${input}'*'`)

for i in "${array[@]}"
do :
    echo $i
done
user1357768
fonte
1
Obrigado. muito. Mas, como @anishsane apontou, espaços vazios em nome de arquivo devem ser considerados em meu programa. De qualquer forma, obrigado!
Juneyoung Oh
-3

Para mim, isso funcionou bem no cygwin:

declare -a names=$(echo "("; find <path> <other options> -printf '"%p" '; echo ")")
for nm in "${names[@]}"
do
    echo "$nm"
done

Isso funciona com espaços, mas não com aspas duplas (") nos nomes dos diretórios (que não são permitidos em um ambiente Windows de qualquer maneira).

Cuidado com o espaço na opção -printf.

R Risack
fonte
3
Quebrado e perigoso : não lida com citações, e está sujeito a injeção arbitrária de código. NÃO USE.
gniourf_gniourf
2
Parece que alguém sinalizou esta postagem para exclusão. "Está errado" não é motivo para exclusão do SO. O usuário tentou responder, está no assunto e atende aos critérios de resposta. O botão downvote é usado para medir a utilidade e correção, não o botão de exclusão.
Frambot
3
Como gniourf apontou, não é para ambientes onde outras pessoas inserem as opções em seu sistema, por exemplo, páginas da web. Mas nem todo mundo programa para esse ambiente. Usei-o para renomear arquivos em diretórios.
R Risack