Como posso recursivamente substituir uma string em nomes de arquivos e diretórios?

1

Eu tenho um git clone do etckeeper, e estou tentando renomear todos os arquivos e diretórios com etckeeper no nome para usrkeeper . Por exemplo, ./foo-etckeeper-bar deve ser renomeado para ./foo-usrkeeper-bar .

Encontrar os arquivos relevantes é trivial:

% find . -path '*etckeeper*' -print

No entanto, não consigo descobrir como realmente fazer a renomeação. Eu tentei combinar xargs com mv :

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c mv % '$(echo' % \| sed \"s/etckeeper/usrkeeper/\" \)

Para facilitar a leitura, a segunda metade sem escape indica: xargs -0 -n 1 -J % bash -c mv % $(echo % | sed "s/etckeeper/usrkeeper/" ) . A ideia por trás disso é que usamos $() para canalizar o nome do arquivo através de sed , que é usado para fazer a substituição.

O problema aqui é que bash -c requer que o comando seja executado para ser uma única string. Depois disso, ele começa interpretando argumentos como parâmetros posicionais. Eu poderia citar a coisa toda:

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c 'mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

Mas agora xargs não substituirá % . Como posso resolver isso? (Além disso, como uma nota lateral, o acima falhará se houver um arquivo contendo etckeeper no nome em um diretório contendo etckeeper no nome, porque o diretório será movido antes do arquivo.)

    
por strugee 10.05.2016 / 22:37

3 respostas

4

Você pode usar o comando Renomear (consulte editar 1 ).

Solução 1

Para um número razoável de arquivos / diretório, definindo a opção 4 globstar (não funciona em nome recursivo, veja editar 3 ):

shopt -s globstar
rename -n 's/etckeeper/userkeeper/g' **

Solução 2

Para um grande número de arquivos / diretórios usando renomear e encontrar em duas etapas para evitar renomear arquivos com falha em apenas diretórios renomeados (veja editar 2 ):

find . -type f -exec rename 's/etckeeper/userkeeper/g' {} \;
find . -type d -exec rename 's/etckeeper/userkeeper/g' {} \;

EDIT 1:

Existem dois comandos diferentes de renomeação . Esta resposta usa o comando rename baseado em Perl, disponível em distribuições baseadas no Debian. Para tê-lo em uma distro não baseada em Debian, você pode instalar do cpan ou acessá-lo.

EDIT 2:

Como apontado por Jeff Schaller nos comentários, a opção -depth find Process each directory's contents before the directory itself , então apenas uma opção "ordenada" encontrada por -depth seria suficiente:

find . -depth -exec rename 's/etckeeper/userkeeper/g' {} \;

EDIT 3

A solução 1 não funciona para alvos de renomeação recursiva, (es. etckeeper/etckeeper/etckeeper ) porque os níveis externos são processados antes que os níveis internos e ponteiro para níveis internos se tornem inúteis. (Depois que o primeiro renomear etckeeper/etckeeper/etckeeper será nomeado usrkeeper/etckeeper/etckeeper , a renomeação para etckeeper/etckeeper/ e etckeeper/etckeeper/etckeeper falhará). O mesmo problema corrigido nas opções find by -depth .

EDIT4

Como apontado nos comentários por cas, eu usaria {} + em vez de {} \; - Forçar um script perl como renomear várias vezes (uma vez por arquivo / dir) é caro.

    
por 10.05.2016 / 23:16
1

A sua pergunta original é realmente fácil de responder: xargs (pelo menos no OS X) também tem uma opção -I , o que não requer que a cadeia de substituição seja única argumento. Então, a invocação simplesmente se torna:

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

Fácil peasy. Agora, vamos renomear na ordem correta. Como se constata, find irá imprimir os diretórios primeiro (porque tem que processá-los antes de descer para eles), então tudo que precisamos fazer é reverter a ordem dos nomes dos arquivos:

% find . -path '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

Note que eu mudei find -print0 | xargs -0 para find -print | xargs . Por quê? Porque tail -r reverte com base em novas linhas, não em caracteres nulos. Se você não alterou, tail -r imprimiria a mesma coisa que você inseriu nela! Portanto, o script está mais correto agora, mas também será quebrado em nomes de arquivo que contenham, e. novas linhas.

Se você não tiver tail -r , tente tac . Veja: Como posso reverter a ordem das linhas em um arquivo? no Stack Overflow.

Agora, a questão é que o comando sed é muito agressivo. Por exemplo, com uma árvore como:

.
├── etckeeper-foo
│   └── etckeeper-bar.md
└── some-other-directory
    └── etckeeper-baz.md

Você receberá mv invocações semelhantes a:

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./usrkeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

O primeiro está bem, mas o segundo não está claro - já que ainda não fizemos mv ./etckeeper-foo ./usrkeeper-foo !

Vamos apenas substituir no último componente do nome do caminho. Podemos fazer isso usando basename e dirname :

% find . -name '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(dirname %)/$(basename % | sed "s/etckeeper/usrkeeper/g" )'

Observe que a outra alteração que fiz foi usar find -name , não find -path .

Isso produz as invocações corretas de mv :

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./etckeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

Finalmente. Lembre-se, mais uma vez, que isso falhará em nomes de arquivos esotéricos. Observe também que, se os seus nomes de arquivos forem particularmente longos, xargs não funcionará corretamente porque os argumentos para mv se tornam muito longos. Você pode (pelo menos no OS X) passar -x para xargs para dizer a ele para falhar imediatamente se este for o caso.

    
por 10.05.2016 / 22:37
0

Outra solução é usar um script pequeno e fazer um loop for nos resultados de pesquisa e um mv com uma substituição de string bash nos arquivos encontrados:

IFS=$'\n'
for files in $(find . -name "*etckeeper*"); 
do 
  mv "$files ${files/etckeeper/usrkeeper}"
done

Se você não usá-lo no script, salve melhor o IFS original e restaure-o no final do loop.

    
por 10.05.2016 / 23:03