
Digamos que tengo dos archivos de texto src.txt
y dest.txt
, donde src.txt
contiene una lista de nombres de archivos (algunos de los cuales incluyen espacios) /src/dir/
y dest.txt
contiene, en orden aleatorio, una lista de las rutas completas de los archivos (nuevamente con espacios) a las que pertenecen. Por ejemplo:
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
¿Cómo puedo realizar esta operación de movimiento por lotes desde el shell? He estado trabajando con un while read
bucle sobre el archivo fuente y estoy bastante seguro de que necesito usar el mv
comando, pero no estoy seguro de si grep
son sed
necesarios aquí. Sigo encontrando cannot stat...
errores de resolución de caracteres de espacio.
Respuesta1
Con zsh
:
src=(${(f)"$(<src.txt)"})
for f (${(f)"$(<dest.txt)"})
(($src[(Ie)$f:t])) && mv /src/dir/$f:t $f
Esto lee cada archivo en una matriz y luego, para cada elemento en la"destino"matriz, si el nombre base ( :t
es un zsh
modificador que elimina todos los componentes principales del nombre de ruta) también está en el"src"matriz luego mueve el archivo. Para realizar un funcionamiento en seco, reemplácelo mv
con printf '"%s" -> "%s"\n'
.
Ahora, también puedes ejecutar (aún en zsh
):
for f (${(f)"$(grep -Ff src.txt dest.txt)"})
mv /src/dir/$f:t $f
lo cual funciona bien siempre que ninguno de los nombres de archivos src.txt
coincida con ninguno de los nombres de directorio (o parte de ese nombre) en la lista de rutas (por ejemplo , dest.txt
un nombre de archivo y una ruta como en daría un falso positivo). Para evitar eso, puede pasar los nombres de los archivos como patrones (es decir, usando expresiones regulares como ) en lugar de cadenas fijas para que coincidan solo con el último componente de las rutas en . Aunque eso requiere escapar de todos los caracteres especiales (si los hay) en los nombres de archivos , por ejemplo, esta vez con ( ):data1
src.txt
/path/data1_dir/some_file
dest.txt
grep
/filename$
F
dest.txt
src.txt
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
Respuesta2
Si una nueva línea es un delimitador aceptable, entonces lo siguiente debería ser bastante sólido en un shell POSIX:
IFS='
';set -f
for f in $(cat <"$destfile")
do [ -e "./${f##*/}" ] ||
[ -h "./${f##*/}" ] &&
mv "./${f##*/}" "$f"
done
Hay dos posibles problemas con esa solución que puedo imaginar:
El tamaño del archivo de entrada es simplemente demasiado grande para dividirlo de una sola vez.
- En mi sistema, esto ni siquiera merece una consideración seria hasta que la entrada se acerca a muchas decenas de miles de líneas.
Es posible que exista un nombre de archivo
$destfile
en el directorio actual y aún así deberíanoser movido de todos modos.- Debido a que esta solución renuncia por completo a comparar los dos archivos de entrada y solo verifica
$destfile
la existencia de cada último componente de nombre de ruta en el directorio actual, si algún nombre de archivo puede coincidir involuntariamente, no debe considerarse.
- Debido a que esta solución renuncia por completo a comparar los dos archivos de entrada y solo verifica
Si sólo es necesario solucionar el primer problema:
sed -ne"s|'|'"'\\&&|g' <"$destfile" \
-e "s|.*/\([^/].*\)|_mv './\1' '&'|p" |
sh -c '_mv(){ [ -e "$1" ]||[ -h "$1" ]&& mv "$@";};. /dev/fd/0'
Si sh
es así, dash
puedes dejar el . /dev/fd/0
al final y usar:
sed ... | sh -cs '_mv(){ ...;}'
...porque dash
extrañamente maneja las opciones de invocación de línea de comandos y estándar en conjunto y sin quejarse. Eso no sería muy portátil, pero . /dev/fd/0
, aunque bastante portátil, tampoco cumple estrictamente con los estándares.
Si el segundo problema es motivo de preocupación:
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
... eso debería manejarlo muy bien siempre que todos los nombres de archivos ./"$srcfile"
estén contabilizados de manera adecuada e idéntica al final de alguna ruta en "$destfile"
. sort
siempre hará flotar la más corta de dos comparaciones idénticas en la parte superior, por lo que cuando solo importa el primer campo y el nombre del archivo se antepone al encabezado de cada nombre de ruta, "$destfile"
una operación fusionada sort
de ambos archivos generará secuencias como:
$srcfile: no /
$destfile: match
$destfile: unique
$destfile: unique
...
$srcfile: no /
$destfile: match
$destfile: unique
...y por lo tanto sólo necesita preocuparse por los pares de líneas que comienzan con una que no coincide /
.
Respuesta3
while read i; do echo cp \""$i"\" \"$(grep "/$i$" dst.txt)\"; done < src.txt
Esto imprimirá lo que se habría hecho. Simplemente deshazte de él echo
para copiar los archivos.
Respuesta4
Un guión de una sola línea genera un guión que genera un guión.
En este ejemplo, utilizamos una primera invocación de sed
on src.txt
para generar un segundo sed
script que se ejecutará dest.txt
para generar un script de shell para copiar los archivos.
Aquí está la frase:
$ sed -n "$(sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt)" dest.txt #| sh -x
y la salida:
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";
Tenga en cuenta el comentario #| sh
al final del comando. De esta manera, puede probar el comando y ver qué hace y, si es bueno, descomentar la canalización sh
y copiar realmente los archivos.
El comando sed interno crea un script sed a partir de src.txt. La primera línea del script generado se ve así:
/\/file 1.jpg$/ { s/^/cp file 1.jpg /; p; }
Así es como funciona:
Aporte:
$ 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
Primera sed
invocación. Esto muestra el script generado que será interpretado por la segunda invocación 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; }
Utilice la sustitución de comandos de shell para utilizar la salida del primer sed
comando como un script en la línea de comando pasada a la segunda invocación 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";
Ahora, canalice la salida de sed al shell, con la opción xtrace ( sh -x
). No tengo ninguno de los archivos, de ahí los errores:
$ 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