Eu tenho vários arquivos XML em uma árvore de diretórios que gostaria de mover para as pastas correspondentes com o mesmo nome na mesma árvore de diretórios.
Aqui está um exemplo de estrutura (em shell):
touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
Então minha abordagem aqui é:
find . -name "*.xml" -exec sh -c '
DST=$(
find . -type d -name "$(basename "{}" .xml)" -print -quit
)
[ -d "$DST" ] && mv -v "{}" "$DST/"' ';'
que fornece a seguinte saída:
‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’
mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file
‘./bar.xml’ -> ‘./bar/bar.xml’
‘./foo.xml’ -> ‘./foo/foo.xml’
Mas o arquivo entre colchetes ( [ foo ].xml
) não foi movido como se tivesse sido ignorado.
Já verifiquei e basename
(ex. basename "[ foo ].xml" ".xml"
) converte o arquivo corretamente, porém find
tem problemas com colchetes. Por exemplo:
find . -name '[ foo ].xml'
não encontrará o arquivo corretamente. Porém, ao escapar dos colchetes ( '\[ foo \].xml'
), funciona bem, mas não resolve o problema, pois faz parte do script e não sei quais arquivos possuem esses caracteres especiais (shell?). Testado com BSD e GNU find
.
Existe alguma maneira universal de escapar dos nomes de arquivos ao usar o parâmetro with find
, -name
para que eu possa corrigir meu comando para suportar arquivos com os metacaracteres?
Responder1
É muito mais fácil com zsh
globs aqui:
for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))
Ou se você quiser incluir arquivos xml ocultos e procurar dentro de diretórios ocultos, como find
faria:
for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
Mas cuidado, pois os arquivos chamados .xml
, ..xml
ou ...xml
podem se tornar um problema, então você pode querer excluí-los:
setopt extendedglob
for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
Com as ferramentas GNU, outra abordagem para evitar a varredura de toda a árvore de diretórios em busca de cada arquivo seria varrê-la uma vez e procurar todos os diretórios e xml
arquivos, registrar onde eles estão e fazer a movimentação no final:
(export LC_ALL=C
find . -mindepth 1 -name '*.xml' ! -name .xml ! \
-name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \
-type d -printf 'D/%P\0' | awk -v RS='\0' -F / '
{
if ($1 == "F") {
root = $NF
sub(/\.xml$/, "", root)
F[root] = substr($0, 3)
} else D[$NF] = substr($0, 3)
}
END {
for (f in F)
if (f in D)
printf "%s\0%s\0", F[f], D[f]
}' | xargs -r0n2 mv -v --
)
Sua abordagem apresenta vários problemas se você quiser permitir qualquer nome de arquivo arbitrário:
- incorporar
{}
no código shell ésempreerrado. E se houver um arquivo chamado,$(rm -rf "$HOME").xml
por exemplo? A maneira correta é passá-los{}
como argumento para o shell script in-line (-exec sh -c 'use as "$1"...' sh {} \;
). - Com GNU
find
(implícito aqui como você está usando-quit
),*.xml
corresponderia apenas a arquivos que consistem em uma sequência de caracteres válidos seguidos por.xml
, de modo que exclui nomes de arquivos que contêm caracteres inválidos no código do idioma atual (por exemplo, nomes de arquivos no conjunto de caracteres errado). A solução para isso é corrigir o local ondeC
cada byte é um caractere válido (isso significa que mensagens de erro serão exibidas em inglês). - Se algum desses
xml
arquivos for do tipo diretório ou link simbólico, isso causaria problemas (afetaria a verificação de diretórios ou quebraria links simbólicos quando movido). Você pode adicionar um-type f
para mover apenas arquivos regulares. - Tiras de substituição de comando (
$(...)
)todoscaracteres de nova linha à direita. Isso causaria problemas com um arquivo chamado,foo.xml
por exemplo. Contornar isso é possível, mas é uma dor:base=$(basename "$1" .xml; echo .); base=${base%??}
. Você pode pelo menos substituirbasename
pelos${var#pattern}
operadores. E evite a substituição de comandos, se possível. - seu problema com nomes de arquivos contendo caracteres curinga ( ,
?
e barra invertida; eles não são especiais para o shell, mas para a correspondência de padrões ( ) feita pela qual é muito semelhante à correspondência de padrões de shell). Você precisaria escapar deles com uma barra invertida.[
*
fnmatch()
find
- o problema com
.xml
,..xml
,...xml
mencionado acima.
Então, se abordarmos todos os itens acima, terminaremos com algo como:
LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \
! -name ...xml -exec sh -c '
for file do
base=${file##*/}
base=${base%.xml}
escaped_base=$(printf "%s\n" "$base" |
sed "s/[[*?\\\\]/\\\\&/g"; echo .)
escaped_base=${escaped_base%??}
find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit
done' sh {} +
Ufa...
Agora, não é tudo. Com -exec ... {} +
, executamos o mínimo sh
possível. Se tivermos sorte, executaremos apenas um, mas se não, após a primeira sh
invocação, teremos movido vários xml
arquivos e find
continuaremos procurando por mais, e poderemos muito bem encontrar os arquivos que temos movidos na primeira rodada novamente (e provavelmente tentarão movê-los para onde estão).
Fora isso, é basicamente a mesma abordagem dos zsh. Algumas outras diferenças notáveis:
- com
zsh
aquele, a lista de arquivos é classificada (por nome de diretório e nome de arquivo), de modo que o diretório de destino é mais ou menos consistente e previsível. Comfind
, é baseado na ordem bruta dos arquivos nos diretórios. - com
zsh
, você receberá uma mensagem de erro se nenhum diretório correspondente para onde mover o arquivo for encontrado, não com afind
abordagem acima. - Com
find
, você receberá mensagens de erro se alguns diretórios não puderem ser percorridos, e não com aquelezsh
.
Uma última nota de advertência. Se o motivo de você obter alguns arquivos com nomes duvidosos é porque a árvore de diretórios pode ser gravada por um adversário, tome cuidado, pois nenhuma das soluções acima é segura se o adversário puder renomear arquivos sob esse comando.
Por exemplo, se você estiver usando o LXDE, o invasor pode criar um arquivo malicioso foo/lxde-rc.xml
, criar uma lxde-rc
pasta, detectar quando você está executando seu comando e substituí-lo lxde-rc
por um link simbólico para sua ~/.config/openbox/
janela durante a corrida (que pode ser tão grande quanto necessário de várias maneiras) entre find
encontrar isso lxde-rc
e mv
fazer o rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")
( foo
também pode ser alterado para aquele link simbólico, fazendo você mudar lxde-rc.xml
para outro lugar).
Provavelmente é impossível contornar isso usando utilitários padrão ou mesmo GNU, você precisaria escrevê-lo em uma linguagem de programação adequada, fazendo uma travessia segura de diretórios e usando renameat()
chamadas de sistema.
Todas as soluções acima também falharão se a árvore de diretórios for profunda o suficiente para que o limite no comprimento dos caminhos fornecidos para a rename()
chamada do sistema feita por mv
seja atingido (causando rename()
falha com ENAMETOOLONG
). Uma solução usando renameat()
também resolveria o problema.
Responder2
Ao usar o script embutido com find ... -exec sh -c ...
, você deve passar find
o resultado para o shell por meio do parâmetro posicional, então não precisa usar em {}
todos os lugares do seu script embutido.
Se você tiver bash
ou zsh
, poderá passar basename
a saída por printf '%q'
:
find . -name "*.xml" -exec bash -c '
for f do
BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
DST=$(find . -type d -name "$BASENAME" -print -quit)
[ -d "$DST" ] && mv -v -- "$f" "$DST/"
done
' bash {} +
Com bash
, você pode usar printf -v BASENAME
, e essa abordagem não funcionará corretamente se o nome do arquivo contiver caracteres de controle ou caracteres não-ascii.
Se quiser que funcione corretamente, você precisa escrever uma função shell para escapar apenas de [
, *
e ?
barra invertida.
Responder3
As boas notícias:
find . -name '[ foo ].xml'
não é interpretado pelo shell, é passado desta forma para o programa find. No entanto, Find interpreta o argumento -name
como um glob
padrão e isso precisa ser levado em consideração.
Se você gosta de ligar find -exec \;
ou melhor find -exec +
, não há shell envolvido.
Se você gosta de processar a find
saída pelo shell, recomendo apenas desabilitar o globbing do nome do arquivo no shell chamando set -f
antes do código em questão e ativá-lo novamente chamando set +f
mais tarde.
Responder4
A seguir está um pipeline relativamente simples e compatível com POSIX. Ele verifica a hierarquia duas vezes, primeiro em busca de diretórios e depois em busca de arquivos regulares *.xml. Uma linha em branco entre as varreduras sinaliza AWK da transição.
O componente AWK mapeia nomes de base para diretórios de destino (se houver vários diretórios com o mesmo nome de base, apenas a primeira travessia será lembrada). Para cada arquivo *.xml, ele imprime uma linha delimitada por tabulações com dois campos: 1) o caminho do arquivo e 2) seu diretório de destino correspondente.
{
find . -type d
echo
find . -type f -name \*.xml
} |
awk -F/ '
!NF { ++i; next }
!i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
i { print $0 "\t" d[$NF] }
' |
while IFS=' ' read -r f d; do
mv -- "$f" "$d"
done
O valor atribuído ao IFS logo antes da leitura é um caractere de tabulação literal, não um espaço.
Aqui está uma transcrição usando o esqueleto touch/mkdir da pergunta original:
$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml