
Digamos que eu tenha dois arquivos de texto src.txt
e dest.txt
, onde src.txt
contém uma lista de nomes de arquivos (alguns dos quais incluem espaços) /src/dir/
e dest.txt
contém, em ordem aleatória, uma lista dos caminhos completos dos arquivos (novamente com espaços) aos quais eles pertencem. Por exemplo:
src.txt:
file 1.jpg
file_2.html
file 3.jpg
destino.txt:
/dest/dir 1/file 3.jpg
/dest/file4.txt
/dest/file 5.txt
/dest/dir 2/file 1.jpg
/dest/file_2.html
Como posso realizar esta operação de movimentação em lote do shell? Tenho trabalhado com um while read
loop no arquivo de origem e tenho certeza de que preciso usar o mv
comando, mas não tenho certeza se grep
é sed
necessário aqui. Continuo encontrando cannot stat...
erros de resolução de caracteres espaciais.
Responder1
Com zsh
:
src=(${(f)"$(<src.txt)"})
for f (${(f)"$(<dest.txt)"})
(($src[(Ie)$f:t])) && mv /src/dir/$f:t $f
Isto lê cada arquivo em um array e então, para cada elemento no"destino"matriz, se o nome base ( :t
é um zsh
modificador que remove todos os componentes principais do nome do caminho) também estiver no"src"array então ele move o arquivo. Para realizar um teste, substitua mv
por printf '"%s" -> "%s"\n'
.
Agora, você também pode executar (ainda em zsh
):
for f (${(f)"$(grep -Ff src.txt dest.txt)"})
mv /src/dir/$f:t $f
que funciona bem, desde que nenhum dos nomes de arquivo src.txt
corresponda a qualquer um dos nomes de diretório (ou parte desse nome) na lista de caminhos dest.txt
(por exemplo, um nome de arquivo data1
in src.txt
e um caminho como /path/data1_dir/some_file
in dest.txt
dariam um falso positivo). Para evitar que você possa passar os nomes dos arquivos grep
como padrões (ou seja, usando regex como /filename$
) em vez de F
strings fixas, de modo a corresponder apenas ao último componente dos caminhos em dest.txt
. Embora isso exija o escape de todos os caracteres especiais (se houver) nos nomes de arquivos src.txt
, por exemplo, desta vez com bash
( 4
):
readarray -t files < <(sed 's|[[\.*^$/]|\\&|g;s|.*|/&$|' src.txt | grep -f- dest.txt)
for f in "${files[@]}"; do mv /src/dir/"${f##*/}" "$f"; done
Responder2
Se uma nova linha for um delimitador aceitável, o seguinte deverá ser bastante robusto em um shell POSIX:
IFS='
';set -f
for f in $(cat <"$destfile")
do [ -e "./${f##*/}" ] ||
[ -h "./${f##*/}" ] &&
mv "./${f##*/}" "$f"
done
Existem dois problemas possíveis com essa solução que posso imaginar:
O tamanho do arquivo de entrada é simplesmente grande demais para ser dividido de uma só vez.
- No meu sistema, isso nem vale a pena considerar seriamente até que a entrada se aproxime de dezenas de milhares de linhas.
Um nome de arquivo
$destfile
pode existir no diretório atual e ainda assim devenãoser movido de qualquer maneira.- Como esta solução dispensa totalmente a comparação dos dois arquivos de entrada e apenas verifica
$destfile
a existência de cada último componente do nome do caminho no diretório atual, se algum nome de arquivo corresponder involuntariamente, isso não deverá ser considerado.
- Como esta solução dispensa totalmente a comparação dos dois arquivos de entrada e apenas verifica
Se apenas o primeiro problema precisar ser resolvido:
sed -ne"s|'|'"'\\&&|g' <"$destfile" \
-e "s|.*/\([^/].*\)|_mv './\1' '&'|p" |
sh -c '_mv(){ [ -e "$1" ]||[ -h "$1" ]&& mv "$@";};. /dev/fd/0'
Se for o seu sh
caso, dash
você pode largar o . /dev/fd/0
no final e usar:
sed ... | sh -cs '_mv(){ ...;}'
... porque dash
estranhamente lida com as opções de linha de comando e de invocação stdin em conjunto e sem reclamar. Isso não seria muito portátil, mas . /dev/fd/0
- embora bastante portátil - também não é estritamente compatível com os padrões.
Se a segunda questão for uma preocupação:
export LC_ALL=C
sed -ne'\|/$|!s|.*/\(.*\)|\1/&|p' <"$destfile" |
sort -t/ -k1,1 - ./"$srcfile" | cut -d/ -f2- |
sed -e "\|/|!N;\|\n.*/|!d" \
-e "s|'|'"'\\&&|g' \
-e "s|\n|' '|;s|.*|mv './&'|" | sh
... isso deve lidar com isso muito bem, desde que todos os nomes de arquivos ./"$srcfile"
sejam contabilizados de maneira adequada e idêntica no final de algum caminho em "$destfile"
. sort
sempre flutuará o menor de duas comparações idênticas para o topo e, portanto, quando apenas o primeiro campo for importante e o nome do arquivo for anexado ao início de cada nome de caminho, "$destfile"
uma operação mesclada sort
de ambos os arquivos produzirá sequências como:
$srcfile: no /
$destfile: match
$destfile: unique
$destfile: unique
...
$srcfile: no /
$destfile: match
$destfile: unique
...e então você só precisa se preocupar com pares de linhas que começam com uma que não corresponde a /
.
Responder3
while read i; do echo cp \""$i"\" \"$(grep "/$i$" dst.txt)\"; done < src.txt
Isso imprimirá o que teria sido feito. Apenas livre-se do echo
para realmente copiar os arquivos.
Responder4
Um script de uma linha gera um script que gera um script.
Neste exemplo, usamos uma primeira invocação de sed
on src.txt
para gerar um segundo sed
script que será executado dest.txt
para gerar um script de shell para copiar os arquivos.
Aqui está a frase:
$ sed -n "$(sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt)" dest.txt #| sh -x
e a saída:
cp "file 3.jpg" "/dest/dir 1/file 3.jpg";
cp "file 1.jpg" "/dest/dir 2/file 1.jpg";
cp "file_2.html" "/dest/file_2.html";
Observe o comentário #| sh
no final do comando. Dessa forma, você pode tentar o comando e ver o que ele fará, e se estiver bom, descomente o pipe sh
e realmente copie os arquivos.
O comando sed interno cria um script sed a partir de src.txt. A primeira linha do script gerado fica assim:
/\/file 1.jpg$/ { s/^/cp file 1.jpg /; p; }
É assim que funciona:
Entrada:
$ cat src.txt
file 1.jpg
file_2.html
file 3.jpg
$ cat dest.txt
/dest/dir 1/file 3.jpg
/dest/file4.txt
/dest/file 5.txt
/dest/dir 2/file 1.jpg
/dest/file_2.html
Primeira sed
invocação. Isso mostra o script gerado que será interpretado pela segunda invocação de sed
:
$ sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt
/\/file 1.jpg$/ { s/^/cp "file 1.jpg" "/; s/$/";/; p; }
/\/file_2.html$/ { s/^/cp "file_2.html" "/; s/$/";/; p; }
/\/file 3.jpg$/ { s/^/cp "file 3.jpg" "/; s/$/";/; p; }
Use a substituição de comando shell para usar a saída do primeiro sed
comando como um script na linha de comando passada para a segunda invocação de sed
:
$ sed -n "$(sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt)" dest.txt
cp "file 3.jpg" "/dest/dir 1/file 3.jpg";
cp "file 1.jpg" "/dest/dir 2/file 1.jpg";
cp "file_2.html" "/dest/file_2.html";
Agora, canalize a saída do sed para o shell, com a opção xtrace ( sh -x
). Não tenho nenhum dos arquivos, daí os erros:
$ sed -n "$(sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt)" dest.txt | sh -x
+ cp file 3.jpg /dest/dir 1/file 3.jpg
cp: cannot stat ‘file 3.jpg’: No such file or directory
+ cp file 1.jpg /dest/dir 2/file 1.jpg
cp: cannot stat ‘file 1.jpg’: No such file or directory
+ cp file_2.html /dest/file_2.html
cp: cannot stat ‘file_2.html’: No such file or directory