bash: mover archivos con espacios

10

Cuando muevo un solo archivo con espacios en el nombre del archivo, funciona así:

$ mv "file with spaces.txt" "new_place/file with spaces.txt"

Ahora tengo una lista de archivos que pueden contener espacios y quiero moverlos. Por ejemplo:

$ echo "file with spaces.txt" > file_list.txt
$ for file in $(cat file_list.txt); do mv "$file" "new_place/$file"; done;

mv: cannot stat 'file': No such file or directory
mv: cannot stat 'with': No such file or directory
mv: cannot stat 'spaces.txt': No such file or directory

¿Por qué funciona el primer ejemplo, pero el segundo no? ¿Cómo puedo hacer que funcione?

MrJingles87
fuente

Respuestas:

20

Nunca, nunca lo usesfor foo in $(cat bar) . Este es un error clásico, comúnmente conocido como bash pitfall number 1 . En su lugar, debe usar:

while IFS= read -r file; do mv -- "$file" "new_place/$file"; done < file_list.txt

Cuando se ejecuta el forbucle, golpe del aplicará wordsplitting a lo que lee, lo que significa que a strange blue cloudse puede leer como a, strange, bluey cloud:

$ cat files 
a strange blue cloud.txt
$ for file in $(cat files); do echo "$file"; done
a
strange
blue
cloud.txt

Comparar con:

$ while IFS= read -r file; do echo "$file"; done < files 
a strange blue cloud.txt

O incluso, si insiste en la UUoC :

$ cat files | while IFS= read -r file; do echo "$file"; done
a strange blue cloud.txt

Entonces, el whileciclo leerá su entrada y usará readpara asignar cada línea a una variable. Los IFS=conjuntos El separador de campo de entrada a NULL * , y de la -ropción de readparadas de TI de la interpretación de escapes de barra invertida (de modo que \tse trata como barra + ty no como una pestaña). El --después de que el mvmedio "tratar todo después de que el - como un argumento y no es una opción", que le permite hacer frente a los nombres de archivo que comienzan con -correctamente.


* Esto no es necesario aquí, estrictamente hablando, el único beneficio en este escenario es que evita readeliminar cualquier espacio en blanco al inicio o al final, pero es un buen hábito a tener en cuenta cuando necesita tratar con nombres de archivos que contienen caracteres de nueva línea, o en general, cuando necesita poder tratar con nombres de archivos arbitrarios.

terdon
fuente
1
ok, solo buscaba formas de escapar de la parte "$ archivo". No esperaba el error en otro lugar.
MrJingles87
@ MrJingles87 Lo sé. Este tiene a todos en algún momento. Es muy poco intuitivo si no lo sabes.
terdon
@JohnKugelman sí, sí, buen punto. Respuesta editada, gracias.
terdon
IFS=no ayudará con las nuevas líneas en los nombres de archivo. El formato del archivo aquí simplemente no permite nombres de archivo con nuevas líneas. IFS=es necesario para los nombres de archivos que comienzan en el espacio o en la pestaña (o cualquier otro carácter que no sea el de nueva línea $ IFS contenido de antemano).
Stéphane Chazelas
Ese escollo por lotes se trata más de tratar de analizar la salida de lso findcon sustituciones de comandos cuando hay formas mejores y más confiables de hacerlo. $(cat file)estaría bien cuando se hace bien. Es tan difícil "hacerlo bien" con un ciclo de lectura while como con un + $ (...). Mira mi respuesta.
Stéphane Chazelas
11

Que $(cat file_list.txt)en los shells POSIX como bashen el contexto de la lista es el operador split + glob ( zshsolo hace la parte dividida como cabría esperar).

Se divide en caracteres de $IFS(de forma predeterminada, SPC , TAB y NL) y se globaliza a menos que desactive el globbing por completo.

Aquí, desea dividir solo en nueva línea y no desea la parte global, por lo que debería ser:

IFS='
' # split on newline only
set -o noglob # disable globbing

for file in $(cat file_list.txt); do # split+glob
  mv -- "$file" "new_place/$file"
done

Eso también tiene la ventaja (sobre un while readbucle) de descartar líneas vacías, preservar una línea final no terminada y preservar mvla entrada estándar (necesaria en caso de avisos, por ejemplo).

Sin embargo, tiene la desventaja de que el contenido completo del archivo debe almacenarse en la memoria (varias veces con shells como bashy zsh).

Con algunos shells ( y ksh, zshen menor medida bash), puede optimizarlo en $(<file_list.txt)lugar de hacerlo $(cat file_list.txt).

Para hacer el equivalente con un while readbucle, necesitarías:

while IFS= read <&3 -r file || [ -n "$file" ]; do
  {
    [ -n "$file" ] || mv -- "$file" "new_place/$file"
  } 3<&-
done 3< file_list.txt

O con bash:

readarray -t files < file_list.txt &&
for file in "${files[@]}"
  [ -n "$file" ] || mv -- "$file" "new_place/$file"
done

O con zsh:

for file in ${(f)"$(<file_list.txt)"}
  mv -- "$file" "new_place/$file"
done

O con GNU mvy zsh:

mv -t -- new_place ${(f)"$(<file_list.txt)"}

O con GNU mvy GNU xargsy ksh / zsh / bash:

xargs -rd '\n' -a <(grep . file_list.txt) mv -t -- new_place

Más información sobre lo que significa dejar las expansiones sin citar en Implicaciones de seguridad al olvidar citar una variable en bash / POSIX shells

Stéphane Chazelas
fuente
Sin embargo, creo que debería reconocer que $(cat file_list.txt)lee todo el archivo en la memoria antes de iterar, mientras que while read ...solo lee una línea a la vez. Esto podría ser un problema para archivos grandes.
chepner
@chepner. Buen punto. Adicional.
Stéphane Chazelas
1

En lugar de escribir un guión, puede usar find ..

 find -type f -iname \*.txt -print0 | xargs -IF -0 mv F /folder/to/destination/

para el caso en que los archivos se encuentran en el archivo, puede hacer lo siguiente:

cat file_with_spaces.txt | xargs -IF -0 mv F /folder/to/destination

el segundo no estoy seguro ..

Buena suerte

Noel Alex Makumuli
fuente
3
Si está usando find, también puede usarlo findpara todo. ¿Por qué traerlo xargs? find -type f -iname '*.txt' -exec mv {} /folder/to/destination/.
terdon
xargs simplemente manipula los resultados de find here: -I asigna todos los resultados a una variable F mientras que 0 e print0 simplemente garantizan que no se escapen los archivos con espacios. y -0 evita que los xargs escapen de los resultados. no estoy seguro de cómo -exec se comporta con espacios ..
Noel Alex Makumuli
2
-execse comporta perfectamente con nombres de archivo arbitrarios (incluidos los que contienen espacios, líneas nuevas o cualquier otra rareza). Sé cómo xargsfunciona, gracias :) Simplemente no veo el punto de llamar a un comando separado cuando findya puedo hacerlo por ti. No tiene nada de malo, simplemente ineficiente.
terdon
@terdony tiene razón ... -exec {} / ruta / destino funciona sin problemas ... prefiero xargs debido a las cosas adicionales que puedo hacer con él.
Noel Alex Makumuli
xargstambién tiene el inconveniente de golpear stdin (que mvnecesita para sus indicaciones). También un problema con la solución de @terdon. Con GNU xargsy conchas con sustitución de procesos, se puede aliviar conxargs -0IF -a <(find...) mv F /dest
Stéphane Chazelas
-1
FIRST INSTALL 

apt-get install chromium-browser

apt-get install omxplayer

apt-get install terminator

apt-get install nano (if not already installed)

apt-get install zenity (if not already installed)

THEN CREATE A BASH SCRIPT OF ALL OF THESE PERSONALLY WRITTEN SCRIPTS. 

MAKE SURE THAT EVERY SHELL SCRIPT IS A .sh FILE ENDING EACH ONE OF THESE IS SCRIPTED TO OPEN UP A DIRECTORY FOR YOU TO FIND AND PICK WHAT YOU WANT. 

IT RUNS A BACKGROUND TERMINAL & ALLOWS YOU TO CONTROL THE SONG OR MOVIE.  

MOVIE KEYS ARE AS FOLLOWS; p or space bar for pause, q for quit, - & + for sound up and down, left arrow and right arrow for skipping forward and back. 

MUSIC PLAYER REALLY ONLY HAS A SKIP SONG AND THAT IS CTRL+C AND IF THAT IS THE LAST SONG OR ONLY SONG THEN IT SHUTS DOWN AND THE TERMINAL GOES AWAY.


****INSTRUCTIONS TO MAKE THE SCRIPTS****
- OPEN UP A TERMINAL 

- CD TO THE DIRECTORY YOU WANT THE SCRIPTS TO BE
cd /home/pi/Desktop/

- OPEN UP THE NANO EDITOR WITH THE TITLE OF SHELL YOU WANT
sudo nano Movie_Player.sh

- INSIDE NANO, TYPE OR COPY/PAST (REMEMBER THAT IN TERMINAL YOU NEED TO CTRL+SHIFT+V) THE SCRITP

- SAVE THE DATA WITH CTRL+O
ctrl+o

- HIT ENTER TO SAVE AS THAT FILE NAME OR DELETE THE FILE NAME THEN TYPE NEW ONE JUST MAKE SURE IT ENDS IN .sh THEN HIT ENTER

- NEXT YOU NEED TO SUDO CHMOD IT WITH +X TO MAKE IT CLICKABLE AS A BASH
sudo chmod +x Movie_Player.sh

- FINALLY RUN IT TO TEST EITHER BY DOUBLE CLICKING IT AND CHOOSING "EXECUTE IN TERMINAL" OR BY ./ IT IN TERMINAL
./Movie_Player.sh

YOU ARE NOW GOOD TO PICK A MOVIE OR SELECT A SONG OR ALBUM AND ENJOY!  




**** ALL OF THESE SCRIPTS ACCOUNT FOR SPACES IN THE FILENAME 
SO YOU CAN ACCESS "TOM PETTY" OR "SIXTEEN STONE" WITHOUT NEEDING THE " _ " BETWEEN THE WORDS.




-------WATCH A MOVIE SCRIPT ------- (

#! / bin / bash

ARCHIVO =zenity --title "Pick a Movie" --file-selection

para ARCHIVO en "$ {ARCHIVO [0]}"

hacer omxplayer "$ {FILE [0]}" hecho

--------LISTEN TO A SONG -------

! / bin / bash

ARCHIVO =zenity --title "Pick a Song" --file-selection

para ARCHIVO en "$ {ARCHIVO [0]}"

jugar "$ {FILE [0]}" hecho

--------LISTEN TO AN ALBUM ---------- SONGS IN ORDER

! / bin / bash

DIR =zenity --title "Pick a Album" --file-selection --directory

para DIR en "$ {DIR [0]}"

haga el cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | sort --version-sort | mientras lee DIR; jugar "$ DIR" hecho hecho

--------LISTEN TO AN ALBUM WITH SONGS IN RANDOM ORDER --------

! / bin / bash

DIR =zenity --title "Pick a Album" --file-selection --directory

para DIR en "$ {DIR [0]}"

haga el cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | mientras lee DIR; jugar "$ DIR" hecho hecho

Alfred Gauf
fuente
Lea acerca del descuento y aplique un poco de maquillaje a su respuesta. Y explique por qué esto responde la pregunta. Y no grites !!!
Tengo la sensación de que esta respuesta no pertenece a esta pregunta. Pero si es así, va demasiado lejos, instalar tanto software para un problema tan simple.
MrJingles87