Execute um comando em cada arquivo, com um argumento de sinalização que depende do nome do arquivo

7

Suponha que eu tenha uma pasta contendo arquivos com nomes como

file1.txt
file2.txt
file2.txt

Eu gostaria de executar um comando em cada um deles, assim:

mycommand file1.txt -o file1-processed.txt
mycommand file2.txt -o file2-processed.txt
mycommand file3.txt -o file3-processed.txt

etc.

Existem várias perguntas semelhantes neste site - a diferença é que eu quero inserir o teste -processed no meio do nome do arquivo, antes da extensão.

Parece que find deve ser a ferramenta para o trabalho. Se não fosse pela bandeira -o , eu poderia fazer

find *.txt -exec mycommand "{}" ";"

No entanto, a sintaxe {} fornece o nome do arquivo inteiro, por exemplo, file1.txt etc., por isso não posso adicionar " -processed " entre o nome do arquivo e sua extensão. Existe um problema semelhante ao usar um loop simples bash for .

Existe uma maneira simples de realizar essa tarefa usando find ou de outra forma?

    
por Nathaniel 29.01.2018 / 09:51

6 respostas

25

Se todos os arquivos a serem processados estiverem na mesma pasta, você não precisará usar find e poderá se contentar com a globalização de shell nativa.

for foo in *.txt ; do
  mycommand "${foo}" -o "${foo%.txt}-processed.txt"
done

O idioma do shell ${foo%bar} remove a menor cadeia de sufixos que corresponde ao padrão bar do valor de foo , nesse caso a extensão .txt , para que possamos substituí-la pelo sufixo desejado.

    
por 29.01.2018 / 10:04
6

Se você escreve um script para ser executado em vários sistemas e a portabilidade é uma preocupação, então nada supera for loops e ${var%sfx} da resposta de user1404316 .

No entanto, se você está procurando uma maneira conveniente de fazer coisas semelhantes em seu próprio sistema, então eu recomendo vivamente Paralelo GNU , que é basicamente xargs em esteróides. Aqui está uma solução para sua tarefa específica:

parallel mycommand {} -o {.}-processed.txt ::: *.txt

Será necessária uma lista de strings após ::: (o shell expandirá *.txt para uma lista de nomes de arquivos correspondentes) e executará mycommand {} -o {.}-processed.txt para cada string, substituindo {} por string de entrada (como xargs ) e {.} com nome de arquivo sem extensão.

Você pode alimentar listas de strings de entrada via stdin ( xargs -way), para parear com find ou locate , mas eu raramente preciso de algo mais que zsh ' globs estendidos.

Dê uma olhada no tutorial paralelo GNU para ter uma ideia do que ele pode fazer. Eu uso isso o tempo todo para converter lotes, extraindo arquivos para outros diretórios, etc.

    
por 30.01.2018 / 11:05
4

Isso adapta a resposta do user1404316 para trabalhar com find :

find . -type f -name '*.txt' \
   -exec sh -c 'for file do mycommand "$file" -o "${file%.txt}-processed.txt"; done' sh {} +

(Você pode digitar tudo em uma linha; deixe de fora o \ . Eu quebrei em duas linhas para facilitar a leitura.)

Outra maneira de formatá-lo para facilitar a leitura, torna o script de shell incorporado mais claro:

find . -type f -name '*.txt' -exec sh -c '
  for file
  do
    mycommand "$file" -o "${file%.txt}-processed.txt";
  done
' sh {} +

Basicamente, ele cria um script de shell sem nome:

for file
do
    mycommand "$file" -o "${file%.txt}-processed.txt"
done

(essa é a string entre aspas simples, '…' , desenrolada) e passa para o shell como um comando ( sh -c ) com os nomes de todos os seus arquivos .txt como parâmetros. (Você geralmente não precisa citar {} , e você não precisa de chaves em "$file" .)

    
por 29.01.2018 / 20:53
4

com zsh :

for f (*.txt) mycommand -o $f:r-processed.txt -- $f
  • evite usar opções após argumentos não opcionais. Isso é suportado por alguns comandos, e para a maioria daqueles que fazem (como os que usam a API getopt do GNU), não quando $POSIXLY_CORRECT está no ambiente. Isso também significa que você não pode contornar problemas com nomes de arquivos que começam com - (como fazemos com -- aqui) além de usar ./-file-.txt .
  • for var (values) cmd é uma forma abreviada de loop para reminiscência da sintaxe de perl .
  • zsh é uma dessas invenções raras, onde $f não precisa ser citado (supondo que não esteja em um modo em que esteja emulando outras camadas)
  • $file:r expande para o nome raiz do arquivo, ou seja, a extensão é removida como em csh . É mais ou menos equivalente ao ${f%.*} do shell Korn com o benefício adicional de não ultrapassar / dos limites (com f=./foo , $f:r é ./foo em vez da string vazia), embora não aplique aqui.
  • que exclui arquivos ocultos (como .foo.txt ). Se você quiser incluí-los, poderá adicionar o qualificador D glob ( *.txt(D) ), mas talvez queira excluir um arquivo chamado .txt sozinho, em seguida, o arquivo de saída seria -processed.txt , o que não seria ideal (e também não seria oculto), usando ?*.txt(D) .
  • se não houver nenhum arquivo *.txt no diretório atual, isso falhará com um erro (e, felizmente, não executará mycommand ao contrário de outros shells). Se você quiser que o loop simplesmente não faça nada, adicione o qualificador N glob ( *.txt(N) ).
  • Observe que isso inclui todos os arquivos *.txt , independentemente do seu tipo (regular, link simbólico, diretório, fifo, soquete, dispositivo ...). Se você quiser considerar apenas arquivos regulares , poderá adicionar o qualificador . glob (ou -. para incluir também links simbólicos para arquivos regulares, ou seja, os arquivos para os quais [ -f "$f" ] retornaria verdade). Então, com todos os itens acima aplicados:

    for f (?*.txt(ND.)) mycommand -o $f:r-processed.txt -- $f
    
  • para procurar por *.txt arquivos em subdiretórios, altere-o para **/*.txt (com o D flag também desce em diretórios ocultos).

por 30.01.2018 / 12:28
3

Uma combinação de find , sed e xargs pode funcionar, se você precisar de recursão e uma substituição de cadeia mais complexa:

find . -iname '*.txt' -printf "%p
$ find
.
./bar baz.txt
./foo.txt
$ find . -iname '*.txt' -printf "%p
find . -iname '*.txt' -printf "%p
$ find
.
./bar baz.txt
./foo.txt
$ find . -iname '*.txt' -printf "%p%pre%-o%pre%%p%pre%" | sed -z '3~3s/\.txt$/-processed&/' | xargs -0 -n 3 echo mycommand
mycommand ./bar baz.txt -o ./bar baz-processed.txt
mycommand ./foo.txt -o ./foo-processed.txt
-o%pre%%p%pre%" | sed -z '3~3s/\.txt$/-processed&/' | xargs -0 -n 3 echo mycommand
-o%pre%%p%pre%" | sed -z '3~3s/\.txt$/-processed&/' | xargs -0 -n 3 echo mycommand mycommand ./bar baz.txt -o ./bar baz-processed.txt mycommand ./foo.txt -o ./foo-processed.txt
-o%pre%%p%pre%" | sed -z '3~3s/\.txt$/-processed&/' | xargs -0 -n 3 echo mycommand

Exemplo:

%pre%

O -printf "%p-o-osed%p-processed" imprime o caminho do arquivo duas vezes, com .txt no meio, delimitado com o caractere ASCII NUL, e o comando xargs insere um -o antes de um %code% em cada terceira linha. Então %code% executa o comando com três argumentos por vez, que seria o nome do arquivo, %code% e o nome do arquivo editado.

    
por 29.01.2018 / 10:11
0

Pode-se sempre fazer uso do comando basename , que faz parte do GNU coreutils. Do diretório com os arquivos:

for filename in ./file*.txt
do
    mycommand "$filename" -o ./"$(basename -s '.txt' "$filename" )"-processed.txt
done
    
por 30.01.2018 / 20:33

Tags