loop através de `ls` resulta em script de shell bash

93

Alguém tem um script de shell de modelo para fazer algo com lsuma lista de nomes de diretório e percorrer cada um deles e fazer alguma coisa?

Estou planejando fazer ls -1d */para obter a lista de nomes de diretório.

Daniel A. White
fonte

Respostas:

110

Editado para não usar ls onde uma glob faria, como @ shawn-j-goff e outros sugeriram.

Basta usar um for..do..doneloop:

for f in *; do
  echo "File -> $f"
done

Você pode substituir o *com *.txtou qualquer outro globo que retorne uma lista (de arquivos, diretórios ou qualquer outra coisa), um comando que gera uma lista, por exemplo $(cat filelist.txt), ou substituí-lo por uma lista.

Dentro do doloop, você apenas se refere à variável do loop com o prefixo do cifrão ( $fno exemplo acima). Você pode echofazer ou fazer qualquer outra coisa que desejar.

Por exemplo, para renomear todos os .xmlarquivos no diretório atual para .txt:

for x in *.xml; do 
  t=$(echo $x | sed 's/\.xml$/.txt/'); 
  mv $x $t && echo "moved $x -> $t"
done

Ou melhor ainda, se você estiver usando o Bash, poderá usar as expansões de parâmetro do Bash em vez de gerar um subshell:

for x in *.xml; do 
  t=${x%.xml}.txt
  mv $x $t && echo "moved $x -> $t"
done
CoverosGene
fonte
22
E se o nome do arquivo tiver um espaço?
28720 Daniel A. White
4
Infelizmente, como Daniel disse, o código nesta resposta será interrompido se algum dos arquivos ou pastas contiver um espaço ou nova linha em seu nome. Ele mostra um uso muito comum de forloops e uma armadilha típica de tentar analisar a saída de ls. @ DanielA.White, você pode considerar não aceitar esta resposta se ela não foi útil (ou é potencialmente enganosa), pois como você disse, você está atuando em diretórios. A resposta de Shawn J. Goff deve fornecer uma solução mais robusta e funcional para o seu problema.
slhck
4
-∞ Você não deve analisar a lssaída , não deve ler a saída em forloop , deve usar em $()vez de `` e Usar mais cotações ™ . Tenho certeza que o @CoverosGene teve um bom resultado, mas isso é simplesmente terrível.
precisa saber é o seguinte
1
Uma alternativa melhor se você realmente deseja usar lsé ls -1 | while read line; do stuff; done. Pelo menos esse não será quebrado por espaços em branco.
ejoubaud
1
com mv -vvocê não precisaecho "moved $x -> $t"
DmitrySandalov
68

Usar a saída de lspara obter nomes de arquivos é uma má idéia . Isso pode levar a scripts com mau funcionamento e até perigosos. Isso ocorre porque um nome de arquivo pode conter qualquer caractere, exceto /e o nullcaráter, e lsnão usa qualquer um desses caracteres como delimitadores, por isso, se um nome de arquivo tem um espaço ou uma nova linha, você vai obter resultados inesperados.

Existem duas maneiras muito boas de iterar sobre arquivos. Aqui, usei simplesmente echopara demonstrar fazer algo com o nome do arquivo; você pode usar qualquer coisa.

A primeira é usar os recursos de globbing nativos do shell.

for dir in */; do
  echo "$dir"
done

O shell se expande */em argumentos separados que o forloop lê; mesmo se houver espaço, nova linha ou qualquer outro caractere estranho no nome do arquivo, forcada nome completo será visto como uma unidade atômica; não está analisando a lista de forma alguma.

Se você quiser entrar recursivamente em subdiretórios, isso não funcionará, a menos que o seu shell tenha alguns recursos de globbing estendidos (como basho s globstar. Se o seu shell não tiver esses recursos, ou se você quiser garantir que seu script funcione em uma variedade de sistemas, a próxima opção é usar find.

find . -type d -exec echo '{}' \;

Aqui, o findcomando chamará echoe passará um argumento para o nome do arquivo. Faz isso uma vez para cada arquivo encontrado. Como no exemplo anterior, não há análise de uma lista de nomes de arquivos; em vez disso, um fileneame é enviado completamente como argumento.

A sintaxe do -execargumento parece um pouco engraçada. findpega o primeiro argumento depois -exece trata isso como o programa a ser executado, e todo argumento subsequente, leva como argumento a ser transmitido para esse programa. Existem dois argumentos especiais que -execprecisam ser vistos. O primeiro é {}; esse argumento é substituído por um nome de arquivo findgerado pelas partes anteriores . O segundo é ;, que informa que findeste é o fim da lista de argumentos a serem passados ​​para o programa; findprecisa disso, porque você pode continuar com mais argumentos destinados finde não destinados ao programa exec'd. A razão para \isso é que o shell também trata;especialmente - ele representa o fim de um comando; portanto, precisamos escapar dele para que o shell o dê como argumento, em findvez de consumi-lo por si mesmo; Outra maneira de fazer com que o shell não o trate especialmente é colocá-lo entre aspas: ';'funciona tão bem quanto \;para esse propósito.

Shawn J. Goff
fonte
2
Com +1, esse é definitivamente o caminho a percorrer quando você precisa gerar uma lista de arquivos e usá-la em um comando. O find -exec é limitado apenas por poder executar um único comando. Com o loop, você pode canalizar para o conteúdo do seu coração.
MaQleod
+1. Eu gostaria que mais pessoas usassem find. Há mágica que pode ser feita, e mesmo as limitações percebidas -execpodem ser contornadas. A -print0opção também é valiosa para uso com xargs.
ghoti
A opção loop não mostrará diretórios ocultos. A opção de localização não mostra links simbólicos.
jinawee
16

Para arquivos com espaços, certifique-se de citar a variável como:

 for i in $(ls); do echo "$i"; done; 

ou, você pode alterar a variável de ambiente separador de campo de entrada (IFS):

 IFS=$'\n';for file in $(ls); do echo $i; done

Finalmente, dependendo do que você está fazendo, você pode nem precisar dos ls:

 for i in *; do echo "$i"; done;
John
fonte
bom uso de um subshell no primeiro exemplo
Jeremy L
Por que o IFS precisa de um $ após a atribuição e antes do novo personagem?
Andy Ibanez
3
Os nomes de arquivos também podem conter novas linhas. A interrupção \né insuficiente. Nunca é uma boa idéia recomendar a análise da saída de ls.
ghoti
4

Apenas para adicionar à resposta do CoverosGene, aqui está uma maneira de listar apenas os nomes de diretório:

for f in */; do
  echo "Directory -> $f"
done
n3o
fonte
1

Por que não definir o IFS para uma nova linha e capturar a saída de lsem uma matriz? Definir o IFS como nova linha deve resolver problemas com caracteres engraçados nos nomes de arquivo; O uso lspode ser bom porque possui um recurso de classificação interno.

(Nos testes, eu estava tendo problemas para configurar o IFS, \nmas configurá-lo para o backspace da nova linha funciona, conforme sugerido em outro lugar aqui):

Por exemplo (supondo que o lspadrão de pesquisa desejado seja passado $1):

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")

FILES=($(/bin/ls "$1"))

for AFILE in ${FILES[@]}
do
    ... do something with a file ...
done

IFS=$SAVEIFS

Isso é especialmente útil no OS X, por exemplo, para capturar uma lista de arquivos classificados por data de criação (da mais antiga para a mais nova), o lscomando é ls -t -U -r.

jetset
fonte
2
Os nomes de arquivos também podem conter novas linhas, e geralmente o fazem quando os usuários têm permissão para nomear seus próprios arquivos. A interrupção \né insuficiente. As únicas soluções válidas usam um loop for com expansão do nome do caminho ou find.
ghoti
A única maneira confiável de transferir uma lista de nomes de arquivos é separá-los com um caractere NUL, pois esse é definitivamente o único que não está contido no caminho do arquivo.
glglgl
-3

É assim que eu faço, mas provavelmente existem maneiras mais eficientes.

ls > filelist.txt

while read filename; do echo filename: "$filename"; done < filelist.txt
ÁRVORE
fonte
6
Atenha-se aos canos no lugar do arquivo:>ls | while read i; do echo filename: $i; done
Jeremy L
Legal. Devo dizer que você também pode usar $ EDITOR filelist.txt entre os dois comandos. Muitas coisas que você pode fazer em um editor que é mais fácil do que na linha de comando. Não é relevante para esta questão, no entanto.
ÁRVORE
Sua solução não resolve o problema com nomes de arquivos que contêm novas linhas e outras coisas sofisticadas.
glglgl