bash: déplacement de fichiers avec des espaces

10

Lorsque je déplace un seul fichier avec des espaces dans le nom de fichier, cela fonctionne comme ceci:

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

Maintenant, j'ai une liste de fichiers qui peuvent contenir des espaces et je veux les déplacer. Par exemple:

$ 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

Pourquoi le premier exemple fonctionne-t-il, mais pas le second? Comment puis-je le faire fonctionner?

MrJingles87
la source

Réponses:

20

Jamais, jamais utiliserfor foo in $(cat bar) . Il s'agit d'une erreur classique, communément appelée bash pitfall number 1 . Vous devriez plutôt utiliser:

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

Lorsque vous exécutez la forboucle, bash appliquera wordsplitting à ce qu'il lit, ce qui signifie que a strange blue cloudsera lu comme a, strange, blueet cloud:

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

Comparer aux:

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

Ou même, si vous insistez sur l' UUoC :

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

Ainsi, la whileboucle lira son entrée et utilisera le readpour affecter chaque ligne à une variable. Le IFS=définit le séparateur de champ d'entrée sur NULL * , et l' -roption de l' readempêche d'interpréter les échappements de barre oblique inverse (de sorte que cela \tsoit traité comme une barre oblique + tet non comme un onglet). L' --après le mvsignifie "tout traiter après le - comme un argument et non une option", ce qui vous permet de traiter -correctement les noms de fichiers commençant par .


* Cela n'est pas nécessaire ici, à proprement parler, le seul avantage de ce scénario est qu'il empêche readde supprimer les espaces blancs de début ou de fin, mais c'est une bonne habitude à prendre lorsque vous devez traiter des noms de fichiers contenant des caractères de nouvelle ligne, ou en général, lorsque vous devez être capable de gérer des noms de fichiers arbitraires.

terdon
la source
1
ok, je cherchais uniquement des moyens d'échapper à la partie "$ file". ne s'attendait pas à l'erreur ailleurs.
MrJingles87
@ MrJingles87 je sais. Celui-ci attire tout le monde à un moment donné. C'est très peu intuitif si vous ne le savez pas.
terdon
@JohnKugelman oui en effet, bon point. Réponse modifiée, merci.
terdon
IFS=n'aidera pas avec les nouvelles lignes dans les noms de fichiers. Le format du fichier ici ne permet tout simplement pas les noms de fichiers avec des retours à la ligne. IFS=est nécessaire pour les noms de fichiers commençant par un espace ou une tabulation (ou par tout autre caractère que la nouvelle ligne $ IFS contenue auparavant).
Stéphane Chazelas
Cet écueil par lots consiste davantage à essayer d'analyser la sortie de lsou findavec des substitutions de commandes lorsqu'il existe des moyens meilleurs et plus fiables de le faire. $(cat file)serait bien quand bien fait. Il est à peu près aussi difficile de "bien faire les choses" avec une boucle de lecture while qu'avec une boucle for + $ (...). Voir ma réponse.
Stéphane Chazelas
11

Dans $(cat file_list.txt)les shells POSIX comme bashdans le contexte de la liste, l'opérateur split + glob ( zshne fait que la partie split comme vous vous y attendez).

Il se divise en caractères de $IFS(par défaut, SPC , TAB et NL) et ne fait de glob que si vous désactivez complètement la globalisation.

Ici, vous voulez diviser uniquement sur la nouvelle ligne et ne voulez pas la partie glob, donc ça devrait être:

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

Cela a également l'avantage (sur une while readboucle) de supprimer les lignes vides, de conserver une ligne non terminée et de conserver mvle stdin (nécessaire en cas d'invites par exemple).

Il présente cependant l'inconvénient que le contenu complet du fichier doit être stocké en mémoire (plusieurs fois avec des coques comme bashet zsh).

Avec certains shells ( ksh, zshet dans une moindre mesure bash), vous pouvez l'optimiser avec $(<file_list.txt)au lieu de $(cat file_list.txt).

Pour faire l'équivalent avec une while readboucle, il vous faudrait:

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

Ou avec bash:

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

Ou avec zsh:

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

Ou avec GNU mvet zsh:

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

Ou avec GNU mvet GNU xargset ksh / zsh / bash:

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

Plus d'informations sur ce que signifie laisser des extensions sans guillemets sur les implications de la sécurité d'oublier de citer une variable dans des shells bash / POSIX

Stéphane Chazelas
la source
Je pense que vous devriez reconnaître, cependant, que $(cat file_list.txt)le fichier entier est lu en mémoire avant d'itérer, alors qu'il while read ...ne lit qu'une ligne à la fois. Cela pourrait être un problème pour les fichiers volumineux.
chepner
@chepner. Bon point. Ajoutée.
Stéphane Chazelas
1

Au lieu d'écrire un script, vous pouvez utiliser find ..

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

pour le cas où les fichiers se trouvent dans le fichier, vous pouvez faire ce qui suit:

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

le second pas sûr cependant ..

Bonne chance

Noel Alex Makumuli
la source
3
Si vous utilisez find, vous pouvez tout aussi bien utiliser findpour tout. Pourquoi y entrer xargs? find -type f -iname '*.txt' -exec mv {} /folder/to/destination/.
terdon
xargs manipule simplement les résultats de find ici: -I affecte tous les résultats à une variable F tandis que 0 et print0 garantissent simplement que les fichiers avec des espaces ne sont pas échappés. et -0 empêche les xargs d'échapper aux résultats. ne sais pas comment -exec se comporte avec les espaces ..
Noel Alex Makumuli
2
-execse comporte parfaitement avec des noms de fichiers arbitraires (y compris ceux qui contiennent des espaces, des nouvelles lignes ou toute autre bizarrerie). Je sais comment ça xargsmarche, merci :) Je ne vois tout simplement pas l'intérêt d'appeler une commande distincte quand findje peux déjà le faire pour vous. Rien de mal à cela, juste inefficace.
terdon
@terdonyou a raison .. -exec {} / path / destination fonctionne bien .. je préfère les xargs à cause de plus de choses supplémentaires que je peux faire avec.
Noel Alex Makumuli
xargsa également l'inconvénient de clobber stdin (qui a mvbesoin de ses invites). Également un problème avec la solution de @ terdon. Avec GNU xargset coques avec substitution de processus, peut être allégé avecxargs -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

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

pour FILE dans "$ {FILE [0]}"

faire omxplayer "$ {FILE [0]}" fait

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

! / bin / bash

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

pour FILE dans "$ {FILE [0]}"

jouer "$ {FILE [0]}" fait

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

! / bin / bash

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

pour DIR dans "$ {DIR [0]}"

faites le cd "$ {DIR [0]}" && find. -type f -nom ' .ogg' -o -nom ' .mp3' | sort --version-sort | en lisant DIR; jouer "$ DIR" fait fait

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

! / bin / bash

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

pour DIR dans "$ {DIR [0]}"

faites le cd "$ {DIR [0]}" && find. -type f -nom ' .ogg' -o -nom ' .mp3' | en lisant DIR; jouer "$ DIR" fait fait

Alfred Gauf
la source
Veuillez lire le démarque et appliquer du maquillage à votre réponse. Et veuillez expliquer pourquoi cela répond à la question. ET NE CRIEZ PAS !!!
J'ai l'impression que cette réponse n'appartient pas à cette question. Mais si c'est le cas, cela va très loin, d'installer autant de logiciels pour un problème aussi simple.
MrJingles87