Mover uma lista de arquivos combinando-os com um índice de destino

Mover uma lista de arquivos combinando-os com um índice de destino

Digamos que eu tenha dois arquivos de texto src.txte dest.txt, onde src.txtcontém uma lista de nomes de arquivos (alguns dos quais incluem espaços) /src/dir/e dest.txtconté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 readloop no arquivo de origem e tenho certeza de que preciso usar o mvcomando, mas não tenho certeza se grepé sednecessá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 zshmodificador 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 mvpor 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.txtcorresponda a qualquer um dos nomes de diretório (ou parte desse nome) na lista de caminhos dest.txt(por exemplo, um nome de arquivo data1in src.txte um caminho como /path/data1_dir/some_filein dest.txtdariam um falso positivo). Para evitar que você possa passar os nomes dos arquivos grepcomo padrões (ou seja, usando regex como /filename$) em vez de Fstrings 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 $destfilepode 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 $destfilea 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.

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 shcaso, dashvocê pode largar o . /dev/fd/0no final e usar:

sed ... | sh -cs '_mv(){ ...;}'

... porque dashestranhamente 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". sortsempre 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 sortde 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 echopara 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 sedon src.txtpara gerar um segundo sedscript que será executado dest.txtpara 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 #| shno final do comando. Dessa forma, você pode tentar o comando e ver o que ele fará, e se estiver bom, descomente o pipe she 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 sedinvocaçã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 sedcomando 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

informação relacionada