Como fazer um loop sobre arquivos no diretório e alterar o caminho e adicionar sufixo ao nome do arquivo

562

Preciso escrever um script que inicie meu programa com argumentos diferentes, mas sou novo no Bash. Inicio meu programa com:

./MyProgram.exe Data/data1.txt [Logs/data1_Log.txt].

Aqui está o pseudocódigo do que eu quero fazer:

for each filename in /Data do
  for int i = 0, i = 3, i++
    ./MyProgram.exe Data/filename.txt Logs/filename_Log{i}.txt
  end for
end for

Então, eu estou realmente intrigado em como criar o segundo argumento a partir do primeiro, então parece com dataABCD_Log1.txt e inicio o meu programa.

Dobrobobr
fonte

Respostas:

736

Algumas notas primeiro: quando você usa Data/data1.txtcomo argumento, deveria mesmo ser /Data/data1.txt(com uma barra inicial)? Além disso, o loop externo deve procurar apenas arquivos .txt ou todos os arquivos em / Data? Aqui está uma resposta, assumindo /Data/data1.txtapenas arquivos .txt:

#!/bin/bash
for filename in /Data/*.txt; do
    for ((i=0; i<=3; i++)); do
        ./MyProgram.exe "$filename" "Logs/$(basename "$filename" .txt)_Log$i.txt"
    done
done

Notas:

  • /Data/*.txtexpande para os caminhos dos arquivos de texto em / Data ( incluindo a / Data / parte)
  • $( ... ) executa um comando shell e insere sua saída naquele ponto na linha de comando
  • basename somepath .txtgera a parte base de algum caminho, com .txt removido do final (por exemplo, /Data/file.txt-> file)

Se você precisar executar o MyProgram em Data/file.txtvez de /Data/file.txt, use "${filename#/}"para remover a barra inicial. Por outro lado, se você realmente Datanão /Datadeseja digitalizar, basta usar for filename in Data/*.txt.

Gordon Davisson
fonte
1
Aparentemente, basename -sé uma extensão fora do padrão - editarei minha resposta para usar a sintaxe padrão.
Gordon Davisson
21
Se nenhum arquivo for encontrado / corresponder ao curinga, estou encontrando o bloco de execução de loops for ainda inserido uma vez com filename = "/Data/*.txt". Como posso evitar isso?
Oliver Pearmain 03/02
20
@OliverPearmain Use shopt -s nullglobantes do loop (e shopt -u nullglobdepois para evitar problemas mais tarde) ou adicione if [[ ! -e "$filename ]]; then continue; fino início do loop, para que ele pule arquivos inexistentes.
Gordon Davisson
9
Isso não funciona quando há arquivos que contêm espaços em branco em seus nomes.
Isa Hassen
4
@Isa Deve funcionar com espaço em branco, desde que todas as aspas duplas estejam no lugar. Deixe uma das aspas duplas de lado e você terá problemas com espaços em branco.
Gordon Davisson
345

Desculpe por necromancer o encadeamento, mas sempre que você iterar arquivos por globbing, é uma boa prática evitar a caixa de canto onde o glob não corresponde (o que faz com que a variável de loop se expanda para a própria cadeia de padrão glob (sem correspondência)).

Por exemplo:

for filename in Data/*.txt; do
    [ -e "$filename" ] || continue
    # ... rest of the loop body
done

Referência: Bash Pitfalls

Cong Ma
fonte
8
Este ainda é um aviso oportuno. Eu pensei que tinha criado meu script incorretamente, mas tinha a extensão do arquivo em minúsculas em vez de maiúsculas, ele não encontrou arquivos e retornou o padrão glob. ugh.
RufusVS 14/09
5
Como a tag Bash é usada: shopt nullglobé para isso! (ou shopt failglobpode ser usado também, dependendo do comportamento desejado).
gniourf_gniourf
Além disso, isso também verifica se há arquivos excluídos durante o processamento, antes que o loop os atinja.
Loxaxs 1/1
1
Também lida com o caso em que você tem um diretório chamadodir.txt
vidstige
75
for file in Data/*.txt
do
    for ((i = 0; i < 3; i++))
    do
        name=${file##*/}
        base=${name%.txt}
        ./MyProgram.exe "$file" Logs/"${base}_Log$i.txt"
    done
done

A name=${file##*/}substituição ( expansão de parâmetro do shell ) remove o nome do caminho principal até o último /.

A base=${name%.txt}substituição remove o final .txt. É um pouco mais complicado se as extensões puderem variar.

Jonathan Leffler
fonte
3
Acredito que haja um erro no seu código. A única linha deve ser base=${name%.txt}, em vez de base=${base%.txt}.
Case19lim #
4
@CaseyKlimkowsky: Sim; quando o código e os comentários discordam, pelo menos um deles está errado. Nesse caso, acho que é apenas esse - o código; frequentemente, na verdade, são ambos os que estão errados. Obrigado por apontar isso; Eu consertei isso.
Jonathan Leffler
8

Você pode usar a opção de saída separada com localização nula com a leitura para iterar sobre estruturas de diretório com segurança.

#!/bin/bash
find . -type f -print0 | while IFS= read -r -d $'\0' file; 
  do echo "$file" ;
done

Então, para o seu caso

#!/bin/bash
find . -maxdepth 1 -type f  -print0 | while IFS= read -r -d $'\0' file; do
  for ((i=0; i<=3; i++)); do
    ./MyProgram.exe "$file" 'Logs/'"`basename "$file"`""$i"'.txt'
  done
done

Além disso

#!/bin/bash
while IFS= read -r -d $'\0' file; do
  for ((i=0; i<=3; i++)); do
    ./MyProgram.exe "$file" 'Logs/'"`basename "$file"`""$i"'.txt'
  done
done < <(find . -maxdepth 1 -type f  -print0)

executará o loop while no escopo atual do script (processo) e permitirá que a saída de find seja usada na configuração de variáveis, se necessário

James
fonte
2
$'\0'é uma maneira estranha de escrever ''. Você está perdendo IFS=eo -rinterruptor para read: a sua declaração de leitura deve ser: IFS= read -rd '' file.
gniourf_gniourf 8/02/19
Eu acho que alguns precisariam de pesquisa $'\0'e espalharam alguns pontos da pilha. Indo fazer as edições que você apontou. Quais são os efeitos negativos de não ter IFS=tentado echo -e "ok \nok\0" | while read -d '' line; do echo -e "$line"; done, parece não haver nenhum. Além disso, -r eu vejo muitas vezes é o padrão, mas não consegui encontrar um exemplo para o que impede que isso aconteça.
James
4
IFS=é necessário caso um nome de arquivo termine com um espaço: try is with touch 'Prospero '(observe o espaço à direita). Além disso, você precisa da -ropção caso um nome de arquivo tenha uma barra invertida: experimente touch 'Prospero\n'.
91319 gniourf_gniourf
-1

Parece que você está tentando executar um arquivo do Windows (.exe) Certamente você deve estar usando o PowerShell. De qualquer forma, em um shell bash do Linux, uma simples liner será suficiente.

[/home/$] for filename in /Data/*.txt; do for i in {0..3}; do ./MyProgam.exe  Data/filenameLogs/$filename_log$i.txt; done done

Ou em uma festança

#!/bin/bash

for filename in /Data/*.txt; 
   do
     for i in {0..3}; 
       do ./MyProgam.exe Data/filename.txt Logs/$filename_log$i.txt; 
     done 
 done
user3183111
fonte