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?
Respostas:
Isso ocorre porque
sed
recebe a string{}
como entrada, como pode ser verificado com:find . -exec echo `echo "{}" | sed 's/./foo/g'` \;
que imprime
foofoo
para 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
sed
pipeline de forma afind
executá-lo para todos os arquivos, jáfind
que 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 -c
e uma tonelada de aspas para fazer tudo isso em um comando, mas não vou tentar.)fonte
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 aosmv
pares (xargs -n2
). Esteja ciente de que, neste caso, o caminho em si não deve conter uma stringtest
.fonte
-z
opção junto com achados-print0
e xargs-0
:find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
test
pastas em um caminho.sed
só renomeará o primeiro e omv
comando falhará em caso deNo such file or directory
erro.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
fonte
echo $file | sed s/_test.rb$/_spec.rb/
; feito é uma linha, não é?for
irá 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.-exec
opção de localizar.Eu acho este mais curto
find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;
fonte
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 {} \;
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}
tirassuffix
do valor devar
.ou, para fazer isso usando sed:
for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done
fonte
sed
aquele) conforme explicado pela resposta aceita.for i in... ; do ... ; done
, que executa comandos via shell e não entender crase.Você mencionou que está usando
bash
como seu shell, caso em que você realmente não precisafind
esed
para alcançar a renomeação em lote que você procura ...Supondo que você esteja usando
bash
como shell:$ echo $SHELL /bin/bash $ _
... e presumindo que você habilitou a chamada
globstar
opção shell:$ shopt -p globstar shopt -s globstar $ _
... e finalmente assumindo que você instalou o
rename
utilitário (encontrado noutil-linux-ng
pacote)$ which rename /usr/bin/rename $ _
... então você pode conseguir a renomeação em lote em um bash one-liner da seguinte maneira:
(a
globstar
opção shell garantirá que o bash encontre todos os*_test.rb
arquivos correspondentes , não importa o quão profundamente eles estejam aninhados na hierarquia de diretórios ... usehelp shopt
para descobrir como definir a opção)fonte
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 -b
oucat /proc/meminfo
para descobrir a quantidade de RAM com que precisa trabalhar; Caso contrário, usetop
ou 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/
fonte
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/`"' {} \;
fonte
Para isso você não precisa
sed
. Você pode perfeitamente ficar sozinho com umwhile
loop alimentado com o resultado defind
uma substituição de processo .Portanto, se você tiver uma
find
expressã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á
find
arquivar e renomear todos eles, removendo a string_test.rb
do 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
fonte
while IFS= read -r file; do mv $file ${file%.gz}; done < <(find -type f -name "*.gz")
find .... -exec mv ...
. Além disso, tome cuidado,$file
pois ele falhará se contiver espaços. Melhor usar aspas"$file"
.se você tem Ruby (1.9+)
ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
fonte
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
fonte
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
sed
busca 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
null
delimitado - não tropeça em nenhum caractere em nenhum nome de arquivo, exceto nonull
. 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áGNU
versões desed
efind
para lidar adequadamente com as chamadasfind printf
esed -z -e
e:;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
fork
comsed
, mas eu também estava praticando algumassed
té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}
./app
pode ser indesejado. Exclua-o ou mova-o para outro lugar com antecedência ou, alternativamente, você pode criar uma\( -path PATTERN -exec rm -rf \{\} \)
rotinafind
para fazer isso de forma programática, mas isso é todo seu._mvnfind "${@}"
${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ênciased
à 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
-noclobber
opção definida paramv
; conforme está escrito, esta função não colocará${SRC_DIR}
onde${TGT_DIR}
já existe um.read -R SED <<HEREDOC
find . -name ${OLD} -printf
find
processo. Comfind
, procuramos apenas por qualquer coisa que precise ser renomeada porque já fizemos todas asmv
operações local a local com o primeiro comando da função. Em vez de realizar qualquer ação direta comfind
, como umaexec
chamada, 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:'
find
localizar 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-depth
anexo 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.find
usa 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
find
as saídas de com base em de%directory-depth
modo que os caminhos mais próximos em relação a $ {SRC} sejam trabalhados primeiro. Isso evita possíveis erros envolvendomv
arquivos 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}
%Path
impresso 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 segundosed
processo e, embora um loop curto possa não ser desejável, certamente é melhor do que gerar e bifurcar um processo inteiro.sed
faz 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 modificadastdout
(a qual captura novamente em seguida) e termina o loop.mv
string 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 domv
caminho de destino.sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
-exec
ligações aqui acontecem sem um segundofork
. No primeiro, como vimos, que modificar omv
comando como fornecido porfind
's-printf
comando 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 desed
terminar 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
fonte
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
fonte
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
fonte
$ 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
fonte
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 .
fonte
sed
funciona sem escapar()
se você não definir a-r
opção?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:]]+$//'`' {} +
fonte
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.
fonte
Eu compartilho este post, pois é um pouco relacionado à questão Desculpe por não fornecer mais detalhes. Espero que isso ajude mais alguém. http://www.peteryu.ca/tutorials/shellscripting/batch_rename
fonte
Esta é minha solução de trabalho:
for FILE in {{FILE_PATTERN}}; do echo ${FILE} | mv ${FILE} $(sed 's/{{SOURCE_PATTERN}}/{{TARGET_PATTERN}}/g'); done
fonte