bash: spostamento di file con spazi


10

Quando sposto un singolo file con spazi nel nome del file funziona in questo modo:

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

Ora ho un elenco di file che possono contenere spazi e voglio spostarli. Per esempio:

$ 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

Perché il primo esempio funziona, ma il secondo no? Come posso farlo funzionare?

Risposte:


20

Mai e poi mai usarefor foo in $(cat bar) . Questo è un errore classico, comunemente noto come il numero 1 della trappola di bash . Dovresti invece usare:

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

Quando si esegue il forciclo, bash si applicherà wordsplitting a ciò che si legge, il che significa che a strange blue cloudsarà letto come a, strange, bluee cloud:

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

Confrontare con:

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

O anche, se insisti sull'UUoC :

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

Quindi, il whileciclo leggerà il suo input e utilizzerà il readper assegnare ciascuna riga a una variabile. Gli IFS=insiemi del separatore campo di input NULL * , e l' -ropzione di readimpedisce di interpretare fughe rovesciati (in modo che \tè trattato come barra + te non come una scheda). Il --dopo mvsignifica "tratta tutto dopo - come un argomento e non come un'opzione", che ti consente di gestire i nomi dei file che iniziano -correttamente.


* Questo non è necessario qui, a rigor di termini, l'unico vantaggio in questo scenario è che impedisce readdi rimuovere qualsiasi spazio bianco iniziale o finale, ma è una buona abitudine da prendere quando è necessario occuparsi di nomi di file contenenti caratteri di nuova riga, o in generale, quando è necessario essere in grado di gestire nomi di file arbitrari.


1
ok, cercavo solo modi per sfuggire alla parte "$ file". non mi aspettavo l'errore da qualche altra parte.
MrJingles87,

@ MrJingles87 Lo so. Questo prende tutti ad un certo punto. Non è molto intuitivo se non lo conosci.
Terdon

@JohnKugelman sì davvero, buon punto. Risposta modificata, grazie.
Terdon

IFS=non aiuta con le nuove righe nei nomi dei file. Il formato del file qui semplicemente non consente nomi di file con nuove righe. IFS=è necessario per i nomi dei file che iniziano con lo spazio o la scheda (o qualsiasi altro carattere diverso da newline $ IFS precedentemente contenuto).
Stéphane Chazelas,

Quel trabocchetto discontinuo riguarda più il tentativo di analizzare l'output di lso findcon sostituzioni di comandi quando ci sono modi migliori e più affidabili per farlo. $(cat file)andrebbe bene se fatto bene. È altrettanto difficile "farlo bene" con un ciclo di lettura while così come lo è con quello a + $ (...). Vedi la mia risposta
Stéphane Chazelas,

11

Che $(cat file_list.txt)nelle shell POSIX come bashnel contesto dell'elenco sia l'operatore split + glob ( zshfa solo la parte divisa come ci si aspetterebbe).

Si divide su personaggi di $IFS(per impostazione predefinita, SPC , TAB e NL) e fa glob a meno che non si disattivi del tutto.

Qui, vuoi dividere solo su newline e non vuoi la parte glob, quindi dovrebbe essere:

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

Ciò ha anche il vantaggio (su un while readciclo) di scartare le linee vuote, preservare una linea non terminata finale e preservare lo mvstdin (necessario in caso di prompt, ad esempio).

Ha lo svantaggio, tuttavia, che l'intero contenuto del file deve essere archiviato in memoria (più volte con shell come bashe zsh).

Con alcune shell ( e ksh, zshin misura minore bash), puoi ottimizzarlo con $(<file_list.txt)invece di $(cat file_list.txt).

Per fare l'equivalente con un while readciclo, avresti bisogno di:

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 mve zsh:

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

O con GNU mve GNU xargse ksh / zsh / bash:

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

Altre informazioni su cosa significhi lasciare le espansioni non quotate alle implicazioni sulla sicurezza di dimenticare di citare una variabile nelle shell bash / POSIX


Penso che dovresti riconoscere, tuttavia, che $(cat file_list.txt)legge l'intero file in memoria prima di iterare, mentre while read ...legge solo una riga alla volta. Questo potrebbe essere un problema per file di grandi dimensioni.
Chepner,

@chepner. Buon punto. Aggiunto.
Stéphane Chazelas,

1

Invece di scrivere una sceneggiatura, puoi usare find ..

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

per il caso in cui i file si trovano nel file, è possibile effettuare le seguenti operazioni:

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

il secondo non è sicuro però ..

In bocca al lupo


3
Se stai usando find, puoi anche usare findtutto. Perché portarci xargsdentro? find -type f -iname '*.txt' -exec mv {} /folder/to/destination/.
Terdon

xargs manipola semplicemente i risultati di find here: -I assegna ogni risultato a una variabile F mentre 0 e print0 semplicemente garantiscono che i file con spazi non vengano salvati. e -0 impedisce agli xargs di sfuggire ai risultati. non sono sicuro di come -exec si comporti con gli spazi ..
Noel Alex Makumuli,

2
-execsi comporta perfettamente con nomi di file arbitrari (compresi quelli che contengono spazi, newline o qualsiasi altra stranezza). So come xargsfunziona, grazie :) Non vedo il punto di chiamare un comando separato quando findposso già farlo per te. Niente di sbagliato, solo inefficiente.
Terdon

@terdonyou hai ragione .. -exec {} / path / destination funziona senza problemi .. preferisco gli xargs a causa di più cose extra che sono in grado di fare con esso.
Noel Alex Makumuli,

xargsha anche l'inconveniente di ostruire lo stdin (che ha mvbisogno dei suoi suggerimenti). Anche un problema con la soluzione di @ terdon. Con GNU xargse shell con sostituzione del processo, può essere alleviato 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

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

per FILE in "$ {FILE [0]}"

esegui omxplayer "$ {FILE [0]}" fatto

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

! / Bin / bash

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

per FILE in "$ {FILE [0]}"

gioca "$ {FILE [0]}" fatto

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

! / Bin / bash

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

per DIR in "$ {DIR [0]}"

fai cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | sort --version-sort | mentre leggi DIR; gioca "$ DIR" fatto fatto

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

! / Bin / bash

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

per DIR in "$ {DIR [0]}"

fai cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | mentre leggi DIR; gioca "$ DIR" fatto fatto


Si prega di leggere il markdown e applicare un po 'di trucco alla risposta. E per favore spiega perché questo risponde alla domanda. E NON FARLO !!!

Ho la sensazione che questa risposta non appartenga a questa domanda. Ma se è così, va molto lontano, installare così tanto software per un problema così semplice.
MrJingles87,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.