Renomeie arquivos recursivamente usando find e sed

87

Quero passar por vários diretórios e renomear todos os arquivos que terminam em _test.rb para terminar em _spec.rb. É algo que eu nunca descobri como fazer com o bash, então desta vez eu pensei em colocar algum esforço para acertar. Até agora não consegui, mas meu melhor esforço é:

find spec -name "*_test.rb" -exec echo mv {} `echo {} | sed s/test/spec/` \;

NB: há um eco extra após exec para que o comando seja impresso em vez de executado enquanto estou testando.

Quando eu o executo, a saída para cada nome de arquivo correspondente é:

mv original original

ou seja, a substituição por sed foi perdida. Qual é o truque?

opsb
fonte
BTW, estou ciente de que existe um comando rename, mas eu realmente gostaria de descobrir como fazê-lo usando o sed para que eu possa executar comandos mais poderosos no futuro.
opsb
2
Por favor, não faça postagem cruzada .
Dennis Williamson

Respostas:

32

Isso ocorre porque sedrecebe a string {}como entrada, como pode ser verificado com:

find . -exec echo `echo "{}" | sed 's/./foo/g'` \;

que imprime foofoopara cada arquivo no diretório, recursivamente. A razão para esse comportamento é que o pipeline é executado uma vez, pelo shell, ao expandir todo o comando.

Não há como citar o sedpipeline de forma a findexecutá-lo para todos os arquivos, já findque não executa comandos via shell e não tem noção de pipelines ou crases. O manual GNU findutils explica como realizar uma tarefa semelhante, colocando o pipeline em um script de shell separado:

#!/bin/sh
echo "$1" | sed 's/_test.rb$/_spec.rb/'

(Pode haver uma maneira perversa de usar sh -ce uma tonelada de aspas para fazer tudo isso em um comando, mas não vou tentar.)

Fred Foo
fonte
27
Para aqueles que estão se perguntando sobre o uso perverso de sh -c aqui está: find spec -name "* _test.rb" -exec sh -c 'echo mv "$ 1" "$ (echo" $ 1 "| sed s / test.rb \ $ / spec.rb /) "'_ {} \;
opsb
1
@opsb para que diabos é isso _? ótima solução - mas eu gosto de ramtam responda mais :)
iRaS
Felicidades! Me salvou de muitas dores de cabeça. Para fins de integridade, é assim que o canalizo para um script: find. -name "arquivo" -exec sh /path/to/script.sh {} \;
Sven M.
131

Para resolvê-lo de uma forma mais próxima do problema original, provavelmente seria usar a opção xargs "args por linha de comando":

find . -name "*_test.rb" | sed -e "p;s/test/spec/" | xargs -n2 mv

Ele encontra os arquivos no diretório de trabalho atual recursivamente, ecoa o nome do arquivo original ( p) e então um nome modificado ( s/test/spec/) e alimenta tudo aos mvpares ( xargs -n2). Esteja ciente de que, neste caso, o caminho em si não deve conter uma string test.

Ramtam
fonte
9
Infelizmente, isso tem problemas de espaço em branco. Portanto, usar com pastas que têm espaços no nome irá quebrá-lo em xargs (confirme com -p para o modo verboso / interativo)
cde
1
Isso é exatamente o que eu estava procurando. Muito ruim para o problema do espaço em branco (eu não testei, no entanto). Mas para minhas necessidades atuais é perfeito. Sugiro testá-lo primeiro com "echo" em vez de "mv" como parâmetro em "xargs".
Michele Dall'Agata
5
Se você precisa lidar com espaços em branco em caminhos e está usando GNU sed> = 4.2.2, então você pode usar a -zopção junto com achados -print0e xargs -0:find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
Evan Purkhiser
Melhor solução. Muito mais rápido do que find -exec. Obrigado
Miguel A. Baldi Hörlle
Isso não funcionará se houver várias testpastas em um caminho. sedsó renomeará o primeiro e o mvcomando falhará em caso de No such file or directoryerro.
Casey
22

você pode querer considerar outra forma como

for file in $(find . -name "*_test.rb")
do 
  echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done
ajreal
fonte
Essa parece uma boa maneira de fazer isso. Eu realmente estou procurando quebrar o forro, para melhorar meu conhecimento mais do que qualquer outra coisa.
opsb
2
para o arquivo em $ (find. -name "* _test.rb"); echo mv $ file echo $file | sed s/_test.rb$/_spec.rb/; feito é uma linha, não é?
Bretticus de
5
Isso não funcionará se você tiver nomes de arquivos com espaços. forirá dividi-los em palavras separadas. Você pode fazê-lo funcionar instruindo o loop for para dividir apenas em novas linhas. Consulte cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html para obter exemplos.
Onitake
Eu concordo com @onitake, embora prefira usar a -execopção de localizar.
ShellFish de
18

Eu acho este mais curto

find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;
csg
fonte
Olá, acho que ' _test.rb "deveria ser' _test.rb '(aspas duplas para aspas simples). Posso perguntar por que você está usando o sublinhado para empurrar o argumento que deseja posicionar $ 1 quando me parece que find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;funciona ? Como fariafind . -name '*_test.rb' -exec bash -c 'echo mv $1 ${1/test.rb/spec.rb}' iAmArgumentZero {} \;
agtb
Obrigado por suas sugestões, corrigido
csg
Obrigado por esclarecer isso - só comentei porque passei um tempo refletindo sobre o significado de _ pensando que talvez fosse algum truque de uso de $ _ ('_' é muito difícil de pesquisar em documentos!)
agtb
9

Você pode fazer isso sem sed, se quiser:

for i in `find -name '*_test.rb'` ; do mv $i ${i%%_test.rb}_spec.rb ; done

${var%%suffix}tiras suffixdo valor de var.

ou, para fazer isso usando sed:

for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done
Wayne Conrad
fonte
isso não funciona ( sedaquele) conforme explicado pela resposta aceita.
Ali
@Ali, funciona - eu mesmo testei quando escrevi a resposta. @ explicação de larsman não se aplica a for i in... ; do ... ; done, que executa comandos via shell e não entender crase.
Wayne Conrad
9

Você mencionou que está usando bashcomo seu shell, caso em que você realmente não precisa finde sedpara alcançar a renomeação em lote que você procura ...

Supondo que você esteja usando bashcomo shell:

$ echo $SHELL
/bin/bash
$ _

... e presumindo que você habilitou a chamada globstaropção shell:

$ shopt -p globstar
shopt -s globstar
$ _

... e finalmente assumindo que você instalou o renameutilitário (encontrado no util-linux-ngpacote)

$ which rename
/usr/bin/rename
$ _

... então você pode conseguir a renomeação em lote em um bash one-liner da seguinte maneira:

$ rename _test _spec **/*_test.rb

(a globstaropção shell garantirá que o bash encontre todos os *_test.rbarquivos correspondentes , não importa o quão profundamente eles estejam aninhados na hierarquia de diretórios ... use help shoptpara descobrir como definir a opção)

pvandenberk
fonte
7

A maneira mais fácil :

find . -name "*_test.rb" | xargs rename s/_test/_spec/

A maneira mais rápida (supondo que você tenha 4 processadores):

find . -name "*_test.rb" | xargs -P 4 rename s/_test/_spec/

Se você tiver um grande número de arquivos para processar, é possível que a lista de nomes de arquivos canalizada para xargs faça com que a linha de comando resultante exceda o comprimento máximo permitido.

Você pode verificar o limite do seu sistema usando getconf ARG_MAX

Na maioria dos sistemas Linux, você pode usar free -bou cat /proc/meminfopara descobrir a quantidade de RAM com que precisa trabalhar; Caso contrário, use topou seu aplicativo de monitor de atividade de sistemas.

Uma maneira mais segura (supondo que você tenha 1000000 bytes de RAM para trabalhar):

find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/
13x
fonte
2

Aqui está o que funcionou para mim quando os nomes dos arquivos tinham espaços. O exemplo abaixo renomeia recursivamente todos os arquivos .dar para arquivos .zip:

find . -name "*.dar" -exec bash -c 'mv "$0" "`echo \"$0\" | sed s/.dar/.zip/`"' {} \;
rskengineer
fonte
2

Para isso você não precisa sed. Você pode perfeitamente ficar sozinho com um whileloop alimentado com o resultado de finduma substituição de processo .

Portanto, se você tiver uma findexpressão que seleciona os arquivos necessários, use a sintaxe:

while IFS= read -r file; do
     echo "mv $file ${file%_test.rb}_spec.rb"  # remove "echo" when OK!
done < <(find -name "*_test.rb")

Isso irá findarquivar e renomear todos eles, removendo a string _test.rbdo final e acrescentando _spec.rb.

Para esta etapa, usamos a Expansão de Parâmetros do Shell, onde ${var%string}remove o padrão mais curto "string" de $var.

$ file="HELLOa_test.rbBYE_test.rb"
$ echo "${file%_test.rb}"          # remove _test.rb from the end
HELLOa_test.rbBYE
$ echo "${file%_test.rb}_spec.rb"  # remove _test.rb and append _spec.rb
HELLOa_test.rbBYE_spec.rb

Veja um exemplo:

$ tree
.
├── ab_testArb
├── a_test.rb
├── a_test.rb_test.rb
├── b_test.rb
├── c_test.hello
├── c_test.rb
└── mydir
    └── d_test.rb

$ while IFS= read -r file; do echo "mv $file ${file/_test.rb/_spec.rb}"; done < <(find -name "*_test.rb")
mv ./b_test.rb ./b_spec.rb
mv ./mydir/d_test.rb ./mydir/d_spec.rb
mv ./a_test.rb ./a_spec.rb
mv ./c_test.rb ./c_spec.rb
fedorqui 'então pare de prejudicar'
fonte
Muito obrigado! Isso me ajudou a remover facilmente .gz à direita de todos os nomes de arquivo recursivamente. while IFS= read -r file; do mv $file ${file%.gz}; done < <(find -type f -name "*.gz")
Vinay Vissh
1
@CasualCoder bom ler isso :) Observe que você pode dizer diretamente find .... -exec mv .... Além disso, tome cuidado, $filepois ele falhará se contiver espaços. Melhor usar aspas "$file".
fedorqui 'SO pare de prejudicar'
1

se você tem Ruby (1.9+)

ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
kurumi
fonte
1

Na resposta de ramtam que eu gosto, a parte find funciona bem, mas o restante não funciona se o caminho tiver espaços. Não estou muito familiarizado com o sed, mas fui capaz de modificar essa resposta para:

find . -name "*_test.rb" | perl -pe 's/^((.*_)test.rb)$/"\1" "\2spec.rb"/' | xargs -n2 mv

Eu realmente precisava de uma mudança como esta porque no meu caso de uso, o comando final se parece mais com

find . -name "olddir" | perl -pe 's/^((.*)olddir)$/"\1" "\2new directory"/' | xargs -n2 mv
dzs0000
fonte
1

Não tenho coragem de fazer tudo de novo, mas escrevi isso em resposta ao Commandline Find Sed Exec . Nesse caso, o solicitante queria saber como mover uma árvore inteira, possivelmente excluindo um ou dois diretórios, e renomear todos os arquivos e diretórios contendo a string "OLD" para conter "NEW" .

Além de descrever o como com detalhamento meticuloso a seguir, esse método também pode ser o único que incorpora a depuração embutida. Basicamente, ele não faz nada conforme está escrito, exceto compilar e salvar em uma variável todos os comandos que acredita que deva fazer para executar o trabalho solicitado.

Também evita explicitamente loops tanto quanto possível. Além da sedbusca recursiva por mais de uma correspondência do padrão, não há outra recursão até onde eu sei.

E, por último, isso é totalmente nulldelimitado - não tropeça em nenhum caractere em nenhum nome de arquivo, exceto no null. Eu não acho que você deveria ter isso.

A propósito, isso é MUITO rápido. Veja:

% _mvnfind() { mv -n "${1}" "${2}" && cd "${2}"
> read -r SED <<SED
> :;s|${3}\(.*/[^/]*${5}\)|${4}\1|;t;:;s|\(${5}.*\)${3}|\1${4}|;t;s|^[0-9]*[\t]\(mv.*\)${5}|\1|p
> SED
> find . -name "*${3}*" -printf "%d\tmv %P ${5} %P\000" |
> sort -zg | sed -nz ${SED} | read -r ${6}
> echo <<EOF
> Prepared commands saved in variable: ${6}
> To view do: printf ${6} | tr "\000" "\n"
> To run do: sh <<EORUN
> $(printf ${6} | tr "\000" "\n")
> EORUN
> EOF
> }
% rm -rf "${UNNECESSARY:=/any/dirs/you/dont/want/moved}"
% time ( _mvnfind ${SRC=./test_tree} ${TGT=./mv_tree} \
> ${OLD=google} ${NEW=replacement_word} ${sed_sep=SsEeDd} \
> ${sh_io:=sh_io} ; printf %b\\000 "${sh_io}" | tr "\000" "\n" \
> | wc - ; echo ${sh_io} | tr "\000" "\n" |  tail -n 2 )

   <actual process time used:>
    0.06s user 0.03s system 106% cpu 0.090 total

   <output from wc:>

    Lines  Words  Bytes
    115     362   20691 -

    <output from tail:>

    mv .config/replacement_word-chrome-beta/Default/.../googlestars \
    .config/replacement_word-chrome-beta/Default/.../replacement_wordstars        

NOTA:function Provavelmente, o item acima exigirá GNUversões de sede findpara lidar adequadamente com as chamadas find printfe sed -z -ee :;recursive regex test;t. Se eles não estiverem disponíveis para você, a funcionalidade provavelmente pode ser duplicada com alguns pequenos ajustes.

Isso deve fazer tudo o que você queria do início ao fim, com muito pouco barulho. Eu fiz forkcom sed, mas eu também estava praticando algumas sedtécnicas de ramificação recursiva é por isso que estou aqui. É como conseguir um corte de cabelo com desconto em uma escola de barbeiro, eu acho. Este é o fluxo de trabalho:

  • rm -rf ${UNNECESSARY}
    • Eu deixei intencionalmente de fora qualquer chamada funcional que pudesse excluir ou destruir dados de qualquer tipo. Você mencionou que ./apppode ser indesejado. Exclua-o ou mova-o para outro lugar com antecedência ou, alternativamente, você pode criar uma \( -path PATTERN -exec rm -rf \{\} \)rotina findpara fazer isso de forma programática, mas isso é todo seu.
  • _mvnfind "${@}"
    • Declare seus argumentos e chame a função de trabalho. ${sh_io}é especialmente importante porque salva o retorno da função. ${sed_sep}vem em segundo lugar; esta é uma string arbitrária usada para fazer referência sedà recursão da função. Se ${sed_sep}for definido com um valor que poderia ser potencialmente encontrado em qualquer um dos seus nomes de caminho ou arquivo agidos ... bem, não deixe assim.
  • mv -n $1 $2
    • A árvore inteira é movida desde o início. Isso vai economizar muita dor de cabeça; acredite em mim. O resto do que você deseja fazer - a renomeação - é simplesmente uma questão de metadados do sistema de arquivos. Se você estiver, por exemplo, movendo isso de uma unidade para outra, ou através dos limites do sistema de arquivos de qualquer tipo, é melhor fazer isso de uma vez com um comando. Também é mais seguro. Observe a -noclobberopção definida para mv; conforme está escrito, esta função não colocará ${SRC_DIR}onde ${TGT_DIR}já existe um.
  • read -R SED <<HEREDOC
    • Eu localizei todos os comandos do sed aqui para evitar problemas de escape e os li em uma variável para alimentar o sed abaixo. Explicação abaixo.
  • find . -name ${OLD} -printf
    • Começamos o findprocesso. Com find, procuramos apenas por qualquer coisa que precise ser renomeada porque já fizemos todas as mvoperações local a local com o primeiro comando da função. Em vez de realizar qualquer ação direta com find, como uma execchamada, por exemplo, nós a usamos para construir a linha de comando dinamicamente com -printf.
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • Depois de findlocalizar os arquivos de que precisamos, ele constrói e imprime diretamente (a maior parte ) do comando que precisaremos para processar sua renomeação. O %dir-depthanexo no início de cada linha ajudará a garantir que não estamos tentando renomear um arquivo ou diretório na árvore com um objeto pai que ainda não foi renomeado. findusa todos os tipos de técnicas de otimização para percorrer a árvore do sistema de arquivos e não é certo que retornará os dados de que precisamos em uma ordem segura para operações. É por isso que a seguir ...
  • sort -general-numerical -zero-delimited
    • Classificamos todas findas saídas de com base em de %directory-depthmodo que os caminhos mais próximos em relação a $ {SRC} sejam trabalhados primeiro. Isso evita possíveis erros envolvendo mvarquivos em locais inexistentes e minimiza a necessidade de loop recursivo. ( na verdade, você pode ter dificuldade em encontrar um loop )
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • Acho que este é o único loop em todo o script, e ele apenas faz um loop sobre o segundo %Pathimpresso para cada string no caso de conter mais de um valor $ {OLD} que possa precisar ser substituído. Todas as outras soluções que imaginei envolviam um segundo sedprocesso e, embora um loop curto possa não ser desejável, certamente é melhor do que gerar e bifurcar um processo inteiro.
    • Então, basicamente o que sedfaz aqui é pesquisar $ {sed_sep}, então, tendo encontrado, salva-o e todos os caracteres que encontra até encontrar $ {OLD}, que então substitui por $ {NEW}. Ele então volta para $ {sed_sep} e procura novamente por $ {OLD}, caso ocorra mais de uma vez na string. Se não for encontrado, ele imprime a string modificada stdout(a qual captura novamente em seguida) e termina o loop.
    • Isso evita ter que analisar a string inteira e garante que a primeira metade da mvstring de comando, que precisa incluir $ {OLD}, é claro, inclua-a e a segunda metade seja alterada quantas vezes forem necessárias para limpar o $ {OLD} nome do mvcaminho de destino.
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • As duas -execligações aqui acontecem sem um segundo fork. No primeiro, como vimos, que modificar o mvcomando como fornecido por find's -printfcomando função como necessário alterar corretamente todas as referências de $ {OLD} para o $ {NEW}, mas, a fim de fazê-lo, tivemos que usar algum pontos de referência arbitrários que não devem ser incluídos na saída final. Assim, depois de sedterminar tudo o que precisa fazer, instruímos para limpar seus pontos de referência do buffer de retenção antes de passá-lo adiante.

E AGORA ESTAMOS DE VOLTA

read receberá um comando parecido com este:

% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000

Vai read-lo em ${msg}como ${sh_io}que pode ser examinado no exterior vontade da função.

Legal.

-Mike

mikeserv
fonte
1

Consegui lidar com nomes de arquivos com espaços seguindo os exemplos sugeridos por onitake.

Isso não quebra se o caminho contiver espaços ou a string test:

find . -name "*_test.rb" -print0 | while read -d $'\0' file
do
    echo mv "$file" "$(echo $file | sed s/test/spec/)"
done
James
fonte
1

Este é um exemplo que deve funcionar em todos os casos. Funciona recursivamente, precisa apenas do shell e suporta nomes de arquivos com espaços.

find spec -name "*_test.rb" -print0 | while read -d $'\0' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done
velho
fonte
0
$ find spec -name "*_test.rb"
spec/dir2/a_test.rb
spec/dir1/a_test.rb

$ find spec -name "*_test.rb" | xargs -n 1 /usr/bin/perl -e '($new=$ARGV[0]) =~ s/test/spec/; system(qq(mv),qq(-v), $ARGV[0], $new);'
`spec/dir2/a_test.rb' -> `spec/dir2/a_spec.rb'
`spec/dir1/a_test.rb' -> `spec/dir1/a_spec.rb'

$ find spec -name "*_spec.rb"
spec/dir2/b_spec.rb
spec/dir2/a_spec.rb
spec/dir1/a_spec.rb
spec/dir1/c_spec.rb
Damodharan R
fonte
Ah ... não estou ciente de uma maneira de usar o sed além de colocar a lógica em um script de shell e chamá-lo em exec. não vi o requisito para usar sed inicialmente
Damodharan R
0

Sua pergunta parece ser sobre sed, mas para cumprir seu objetivo de renomeação recursiva, sugiro o seguinte, descaradamente extraída de outra resposta que dei aqui: renomeação recursiva em bash

#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
do
  newf=echo "${f}" | sed -e 's/^(.*_)test.rb$/\1spec.rb/g'
    echo "${f}" "${newf}"
    mv "${f}" "${newf}"
    f="${newf}"
  if [[ -d "${f}" ]]; then
    cd "${f}"
    RecurseDirs $(ls -1 ".")
  fi
done
cd ..
}
RecurseDirs .
Dreynold
fonte
Como sedfunciona sem escapar ()se você não definir a -ropção?
mikeserv
0

Uma maneira mais segura de renomear com find utils e tipo de expressão regular sed:

  mkdir ~/practice

  cd ~/practice

  touch classic.txt.txt

  touch folk.txt.txt

Remova a extensão ".txt.txt" da seguinte maneira -

  cd ~/practice

  find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} \;

Se você usar o + no lugar de; para trabalhar no modo batch, o comando acima renomeará apenas o primeiro arquivo correspondente, mas não a lista inteira de correspondências de arquivo por 'localizar'.

  find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} +
Sathish
fonte
0

Aqui está um bom oneliner que faz o truque. Sed não pode lidar com isso direito, especialmente se várias variáveis ​​são passadas por xargs com -n 2. Uma subestição bash lidaria com isso facilmente como:

find ./spec -type f -name "*_test.rb" -print0 | xargs -0 -I {} sh -c 'export file={}; mv $file ${file/_test.rb/_spec.rb}'

Adicionar -type -f limitará as operações de movimentação apenas para arquivos, -print 0 tratará de espaços vazios nos caminhos.

Orsiris de Jong
fonte
0

Esta é minha solução de trabalho:

for FILE in {{FILE_PATTERN}}; do echo ${FILE} | mv ${FILE} $(sed 's/{{SOURCE_PATTERN}}/{{TARGET_PATTERN}}/g'); done
Antonio Petricca
fonte