bash: memindahkan file dengan spasi

10

Ketika saya memindahkan satu file dengan spasi di nama file itu bekerja seperti ini:

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

Sekarang saya memiliki daftar file yang mungkin berisi spasi dan saya ingin memindahkannya. Sebagai contoh:

$ 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

Mengapa contoh pertama bekerja, tetapi yang kedua tidak? Bagaimana saya bisa membuatnya bekerja?

MrJingles87
sumber

Jawaban:

20

Jangan pernah gunakanfor foo in $(cat bar) . Ini adalah kesalahan klasik, umumnya dikenal sebagai bash pitfall nomor 1 . Anda sebaiknya menggunakan:

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

Ketika Anda menjalankan forlingkaran, bash akan berlaku wordsplitting untuk apa yang dibaca, yang berarti bahwa a strange blue cloudakan dibaca sebagai a, strange, bluedan cloud:

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

Dibandingkan dengan:

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

Atau bahkan, jika Anda bersikeras UUoC :

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

Jadi, whileloop akan membaca inputnya dan menggunakan readuntuk menetapkan setiap baris ke variabel. The IFS=set pemisah field input ke NULL * , dan -rpilihan untuk readberhenti dari menafsirkan lolos backslash (sehingga \tdiperlakukan sebagai slash + tdan bukan sebagai tab). The --setelah mvberarti "memperlakukan semuanya setelah - sebagai argumen dan bukan pilihan", yang memungkinkan Anda berurusan dengan nama file dimulai dengan -benar.


* Ini tidak perlu di sini, secara tegas, satu-satunya manfaat dalam skenario ini adalah menjaga readagar tidak menghapus spasi putih depan atau belakang, tetapi merupakan kebiasaan yang baik untuk dilakukan ketika Anda harus berurusan dengan nama file yang mengandung karakter baris baru, atau secara umum, ketika Anda harus dapat menangani nama file yang sewenang-wenang.

terdon
sumber
1
ok, saya hanya mencari cara untuk keluar dari bagian "$ file". tidak mengharapkan kesalahan di tempat lain.
MrJingles87
@ MrJingles87 Saya tahu. Yang satu ini membuat semua orang pada titik tertentu. Sangat tidak intuitif jika Anda tidak mengetahuinya.
terdon
@ JohnKugelman ya memang, bagus. Jawaban diedit, terima kasih.
terdon
IFS=tidak akan membantu dengan baris baru dalam nama file. Format file di sini tidak memungkinkan nama file dengan baris baru. IFS=diperlukan untuk nama file yang dimulai dengan spasi atau tab (atau karakter apa pun selain baris $ IFS yang terkandung sebelumnya).
Stéphane Chazelas
Itu perangkap batch yang lebih tentang mencoba untuk mengurai output dari lsatau finddengan substitusi perintah ketika ada yang lebih baik, cara yang lebih dapat diandalkan untuk melakukannya. $(cat file)akan baik-baik saja jika dilakukan dengan benar. Ini tentang sama sulitnya untuk "melakukannya dengan benar" dengan loop baca sementara seperti halnya dengan $ + (...) satu. Lihat jawaban saya.
Stéphane Chazelas
11

Bahwa $(cat file_list.txt)dalam shell POSIX seperti bashdalam konteks daftar adalah operator split + glob ( zshhanya melakukan bagian split seperti yang Anda harapkan).

Itu terbagi pada karakter $IFS(secara default, SPC , TAB dan NL) dan melakukan glob kecuali Anda mematikan globbing sama sekali.

Di sini, Anda ingin membagi hanya pada baris baru dan tidak ingin bagian glob, jadi seharusnya:

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

Itu juga memiliki keuntungan (lebih dari satu while readloop) untuk membuang garis kosong, mempertahankan garis trailing yang tidak ditentukan, dan mempertahankan mvstdin (diperlukan jika diminta misalnya).

Itu memang memiliki kekurangan meskipun isi penuh file harus disimpan dalam memori (beberapa kali dengan kerang seperti bashdan zsh).

Dengan beberapa shell ( ksh, zshdan pada tingkat lebih rendah bash), Anda dapat mengoptimalkannya dengan $(<file_list.txt)alih - alih $(cat file_list.txt).

Untuk melakukan hal yang sama dengan while readloop, Anda perlu:

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

Atau dengan bash:

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

Atau dengan zsh:

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

Atau dengan GNU mvdan zsh:

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

Atau dengan GNU mvdan GNU xargsdan ksh / zsh / bash:

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

Lebih banyak membaca tentang apa artinya membiarkan ekspansi tidak dikutip di Implikasi keamanan lupa mengutip variabel dalam bash / POSIX shells

Stéphane Chazelas
sumber
Saya pikir Anda harus mengakui, yang $(cat file_list.txt)membaca seluruh file ke dalam memori sebelum iterasi, sedangkan while read ...hanya membaca satu baris pada satu waktu. Ini bisa menjadi masalah untuk file besar.
chepner
@ chepner. Poin yang bagus. Ditambahkan.
Stéphane Chazelas
1

Alih-alih menulis skrip, Anda dapat menggunakan find ..

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

untuk kasus ketika file berada dalam file maka Anda dapat melakukan hal berikut ::

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

yang kedua tidak yakin ..

Semoga berhasil

Noel Alex Makumuli
sumber
3
Jika Anda menggunakan find, Anda bisa menggunakan findsemuanya. Mengapa membawanya xargs? find -type f -iname '*.txt' -exec mv {} /folder/to/destination/.
terdon
xargs dengan mudah memanipulasi hasil temukan di sini: -Saya menetapkan setiap hasil ke variabel F sementara 0 dan print0 hanya menjamin file dengan spasi tidak lolos. dan -0 mencegah xargs lolos dari hasil. tidak yakin bagaimana -exec berperilaku dengan spasi ..
Noel Alex Makumuli
2
-execberperilaku sempurna dengan nama file yang berubah-ubah (termasuk yang mengandung spasi, baris baru atau keanehan lainnya). Saya tahu cara xargskerjanya, terima kasih :) Saya hanya tidak melihat titik memanggil perintah terpisah ketika findsudah bisa melakukannya untuk Anda. Tidak ada yang salah dengan itu, hanya saja tidak efisien.
terdon
@terdonyou benar .. -exec {} / path / destination bekerja dengan lancar .. saya lebih suka xargs karena lebih banyak hal ekstra yang dapat saya lakukan dengannya.
Noel Alex Makumuli
xargsjuga memiliki kekurangan stdin clobbering (yang mvperlu untuk meminta nya). Juga masalah dengan solusi @ terdon. Dengan GNU xargsdan cangkang dengan proses substitusi, dapat diringankan denganxargs -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

untuk FILE dalam "$ {FILE [0]}"

lakukan omxplayer "$ {FILE [0]}" selesai

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

! / bin / bash

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

untuk FILE dalam "$ {FILE [0]}"

jangan mainkan "$ {FILE [0]}" selesai

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

! / bin / bash

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

untuk DIR dalam "$ {DIR [0]}"

lakukan cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | sort --version-sort | saat membaca DIR; jangan bermain "$ DIR" selesai dilakukan

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

! / bin / bash

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

untuk DIR dalam "$ {DIR [0]}"

lakukan cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | saat membaca DIR; jangan bermain "$ DIR" selesai dilakukan

Alfred Gauf
sumber
Harap baca tentang penurunan harga dan terapkan make-up untuk jawaban Anda. Dan tolong jelaskan mengapa ini menjawab pertanyaan. DAN JANGAN TEMBAK !!!
Saya merasa jawaban ini bukan milik pertanyaan ini. Tetapi jika demikian, maka akan jauh untuk menginstal banyak perangkat lunak untuk masalah yang begitu sederhana.
MrJingles87