bash: przenoszenie plików ze spacjami

10

Kiedy przenoszę pojedynczy plik ze spacjami w nazwie pliku, działa to tak:

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

Teraz mam listę plików, które mogą zawierać spacje i chcę je przenieść. Na przykład:

$ 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

Dlaczego pierwszy przykład działa, a drugi nie? Jak mogę to zrobić?

MrJingles87
źródło

Odpowiedzi:

20

Nigdy, nigdy nie używajfor foo in $(cat bar) . Jest to klasyczny błąd, powszechnie znany jako bash pitfall numer 1 . Zamiast tego powinieneś użyć:

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

Po uruchomieniu forpętli, bash będzie stosować wordsplitting do tego, co czyta, co oznacza, że a strange blue cloudzostanie odczytany jako a, strange, bluei cloud:

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

Porównać do:

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

Lub nawet, jeśli nalegasz na UUoC :

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

whilePętla odczyta zatem dane wejściowe i użyje readdo przypisania każdej linii do zmiennej. W IFS=ustawia separator pola wejściowe NULL * i -ropcji z readwstrzymuje ich interpretacji ucieczki ukośnikowe (tak, że \tsą traktowane jako kreską + t, a nie jako kartę). „ --Po mv” oznacza „traktuj wszystko po - jako argument, a nie jako opcję”, co pozwala ci -poprawnie zajmować się nazwami plików zaczynającymi się od .


* Nie jest to tutaj konieczne, ściśle mówiąc, jedyną korzyścią w tym scenariuszu jest to, że nie readusuwa żadnych początkowych lub końcowych białych znaków, ale dobrym nawykiem jest, aby się do niego stosować, gdy trzeba radzić sobie z nazwami plików zawierającymi znaki nowej linii lub ogólnie, gdy trzeba umieć radzić sobie z dowolnymi nazwami plików.

terdon
źródło
1
ok, szukałem tylko sposobów na ucieczkę części „$ file”. nie spodziewał się błędu gdzie indziej.
MrJingles87
@ MrJingles87 Wiem. Ten w pewnym momencie przyciąga wszystkich . Jest to bardzo nieintuicyjne, jeśli nie wiesz o tym.
terdon
@JKnKugelman tak, rzeczywiście, dobra uwaga. Odpowiedź edytowana, dzięki.
terdon
IFS=nie pomoże z nowymi liniami w nazwach plików. Format pliku tutaj po prostu nie pozwala na nazwy plików z nowymi liniami. IFS=jest potrzebny dla nazw plików rozpoczynających się spacją lub tabulatorem (lub dowolnymi znakami innymi niż nowy znak $ IFS zawarty wcześniej).
Stéphane Chazelas,
W przypadku pułapki wsadowej chodzi bardziej o analizę składni danych wyjściowych lslub findo zamianie poleceń, gdy istnieją lepsze, bardziej niezawodne sposoby wykonania tego. $(cat file)byłoby dobrze, jeśli zrobiono to dobrze. Trudno jest „zrobić to dobrze” za pomocą pętli odczytu na chwilę, tak jak w przypadku pętli za + $ (...). Zobacz moją odpowiedź.
Stéphane Chazelas,
11

To, że $(cat file_list.txt)w powłokach POSIX, podobnie jak bashw kontekście listowym, jest operatorem podziału + glob ( zshwykonuje część dzieloną tylko tak, jak można się spodziewać).

Dzieli się na znaki $IFS(domyślnie SPC , TAB i NL) i robi glob, chyba że całkowicie wyłączysz globowanie.

Tutaj chcesz podzielić tylko na nowy wiersz i nie chcesz części glob, więc powinno to być:

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

Ma to również tę zaletę (nad while readpętlą), że odrzuca puste linie, zachowuje końcową linię niezakończoną i zachowuje mvstandardowe wejście (potrzebne na przykład w przypadku monitów).

Ma to jednak tę wadę, że cała zawartość pliku musi być przechowywana w pamięci (kilka razy w powłokach takich jak bashi zsh).

Z niektórych muszli ( ksh, zshoraz w mniejszym stopniu bash), można zoptymalizować go $(<file_list.txt)zamiast $(cat file_list.txt).

Aby wykonać ekwiwalent za pomocą while readpętli, potrzebujesz:

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

Lub z bash:

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

Lub z zsh:

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

Lub z GNU mvi zsh:

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

Lub z GNU mvi GNU xargsi ksh / zsh / bash:

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

Więcej informacji o tym, co to znaczy pozostawić rozszerzenia bez cudzysłowu na temat wpływu na bezpieczeństwo zapomnienia o cytowaniu zmiennej w powłokach bash / POSIX

Stéphane Chazelas
źródło
Myślę jednak, że powinieneś potwierdzić, że $(cat file_list.txt)odczytuje cały plik do pamięci przed iteracją, podczas gdy while read ...odczytuje tylko jedną linię na raz. Może to stanowić problem w przypadku dużych plików.
chepner
@chepner. Słuszna uwaga. Dodany.
Stéphane Chazelas
1

Zamiast pisać skrypt, możesz użyć find ..

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

w przypadku, gdy pliki znajdują się w pliku, możesz wykonać następujące czynności:

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

drugi nie jestem pewien ...

Powodzenia

Noel Alex Makumuli
źródło
3
Jeśli używasz find, równie dobrze możesz użyć finddo wszystkiego. Po co xargsw to wnosić ? find -type f -iname '*.txt' -exec mv {} /folder/to/destination/.
terdon
xargs po prostu manipuluje wynikami znajdowania tutaj: -Przypisuje każdy wynik do zmiennej F, a 0 i print0 po prostu gwarantują, że pliki ze spacjami nie zostaną zapisane. i -0 zapobiega ucieczce xargs od wyników. nie jestem pewien, jak zachowuje się -exec ze spacjami ..
Noel Alex Makumuli
2
-execzachowuje się doskonale w przypadku dowolnych nazw plików (w tym zawierających spacje, znaki nowej linii lub dowolne inne dziwactwa). Wiem, jak xargsdziała, dziękuję :) Po prostu nie widzę sensu wywoływania osobnego polecenia, kiedy findjuż mogę to zrobić dla ciebie. Nie ma w tym nic złego, po prostu nieefektywny.
terdon
@terdonyou masz rację .. -exec {} / path / destination działa płynnie .. Wolę xargs z powodu dodatkowych rzeczy, które mogę z tym zrobić.
Noel Alex Makumuli
xargsma także wadę stobowania clobberinga (która mvwymaga jego podpowiedzi). Również problem z rozwiązaniem @ terdon. Z GNU xargsi powłokami z substytucją procesu można złagodzić za pomocąxargs -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

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

dla PLIKU w „$ {FILE [0]}”

wykonaj omxplayer „$ {FILE [0]}” gotowe

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

! / bin / bash

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

dla PLIKU w „$ {FILE [0]}”

zagraj „$ {FILE [0]}” gotowe

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

! / bin / bash

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

dla DIR w „$ {DIR [0]}”

wykonaj cd „$ {DIR [0]}” i znajdź. -type f -name ' .ogg' -o -name ' .mp3' | sort --version-sort | podczas czytania DIR; zagraj „$ DIR” gotowe

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

! / bin / bash

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

dla DIR w „$ {DIR [0]}”

wykonaj cd „$ {DIR [0]}” i znajdź. -type f -name ' .ogg' -o -name ' .mp3' | podczas czytania DIR; zagraj „$ DIR” gotowe

Alfred Gauf
źródło
Przeczytaj o obniżce i zastosuj makijaż do swojej odpowiedzi. I proszę wyjaśnić, dlaczego to odpowiada na pytanie. I NIE SHOUT !!!
Mam wrażenie, że ta odpowiedź nie należy do tego pytania. Ale jeśli tak, to bardzo daleko, aby zainstalować tak dużo oprogramowania dla tak prostego problemu.
MrJingles87