Bagaimana cara keluar dari ruang putih dalam daftar lingkaran bash?

121

Saya memiliki skrip bash shell yang melakukan loop melalui semua direktori anak (tetapi bukan file) dari direktori tertentu. Masalahnya adalah beberapa nama direktori mengandung spasi.

Berikut adalah isi direktori pengujian saya:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Dan kode yang melewati direktori:

for f in `find test/* -type d`; do
  echo $f
done

Berikut hasilnya:

tes / Baltimore
tes / Cherry
Bukit
tes / Edison 
tes / Baru
York
Kota
tes / Philadelphia

Cherry Hill dan New York City diperlakukan sebagai 2 atau 3 entri terpisah.

Saya mencoba mengutip nama file, seperti ini:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

tapi tidak berhasil.

Pasti ada cara sederhana untuk melakukan ini.


Jawaban di bawah ini bagus. Tetapi untuk membuatnya lebih rumit - saya tidak selalu ingin menggunakan direktori yang terdaftar di direktori pengujian saya. Terkadang saya ingin memasukkan nama direktori sebagai parameter baris perintah.

Saya mengambil saran Charles untuk mengatur IFS dan menghasilkan yang berikut:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

dan ini berfungsi dengan baik kecuali jika ada spasi di argumen baris perintah (bahkan jika argumen tersebut dikutip). Misalnya, memanggil skrip seperti ini: test.sh "Cherry Hill" "New York City"menghasilkan keluaran berikut:

ceri
Bukit
Baru
York
Kota
MCS
sumber
re: edit, list="$@"membuang sepenuhnya daftar nilai asli, menciutkannya menjadi string. Harap ikuti praktik dalam jawaban saya persis seperti yang diberikan - tugas seperti itu tidak dianjurkan di mana pun di dalamnya; jika Anda ingin meneruskan daftar argumen baris perintah ke program, Anda harus mengumpulkannya ke dalam larik, dan memperluas larik itu secara langsung.
Charles Duffy

Jawaban:

105

Pertama, jangan lakukan seperti itu. Pendekatan terbaik adalah menggunakan find -execdengan benar:

# this is safe
find test -type d -exec echo '{}' +

Pendekatan aman lainnya adalah menggunakan daftar yang diakhiri NUL, meskipun ini memerlukan dukungan find Anda -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Anda juga dapat mengisi array dari find, dan meneruskan array itu nanti:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Jika temuan Anda tidak mendukung -print0, maka hasilnya tidak aman - di bawah ini tidak akan berperilaku seperti yang diinginkan jika ada file yang berisi baris baru dalam namanya (yang, ya, legal):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Jika seseorang tidak akan menggunakan salah satu cara di atas, pendekatan ketiga (kurang efisien dalam hal penggunaan waktu dan memori, karena ia membaca seluruh output dari subproses sebelum melakukan pemisahan kata) adalah dengan menggunakan IFSvariabel yang tidak tidak mengandung karakter spasi. Matikan globbing ( set -f) untuk mencegah string yang berisi karakter glob seperti [], *atau ?diperluas:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Terakhir, untuk kasus parameter baris perintah, Anda harus menggunakan array jika shell Anda mendukungnya (yaitu ksh, bash atau zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

akan mempertahankan pemisahan. Perhatikan bahwa kutipan (dan penggunaan $@daripada $*) itu penting. Array juga dapat diisi dengan cara lain, seperti ekspresi glob:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
Charles Duffy
sumber
1
tidak tahu tentang rasa '+' untuk -exec. manis
Johannes Schaub - litb
1
sepertinya itu juga bisa, seperti xargs, hanya meletakkan argumen di akhir perintah yang diberikan: / itu terkadang mengganggu saya
Johannes Schaub - litb
Menurut saya -exec [name] {} + adalah ekstensi GNU dan 4.4-BSD. (Setidaknya, itu tidak muncul di Solaris 8, dan saya rasa itu juga tidak muncul di AIX 4.3.) Saya kira kita semua mungkin terjebak dengan piping ke xargs ...
Michael Ratanapintha
2
Saya belum pernah melihat sintaks $ '\ n' sebelumnya. Bagaimana cara kerjanya? (Saya akan berpikir bahwa IFS = '\ n' atau IFS = "\ n" akan berfungsi, tetapi tidak juga.)
MCS
1
@crosstalk sudah pasti di Solaris 10, saya baru saja menggunakannya.
Nick
26
find . -type d | while read file; do echo $file; done

Namun, tidak berfungsi jika nama file berisi baris baru. Di atas adalah satu-satunya solusi yang saya tahu ketika Anda benar-benar ingin memiliki nama direktori dalam variabel. Jika Anda hanya ingin menjalankan beberapa perintah, gunakan xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '
Johannes Schaub - litb
sumber
Tidak perlu xargs, lihat temukan -exec ... {} +
Charles Duffy
4
@Charles: untuk file dalam jumlah besar, xargs jauh lebih efisien: hanya memunculkan satu proses. Opsi -exec membagi proses baru untuk setiap file, yang bisa menjadi urutan besarnya lebih lambat.
Adam Rosenfield
1
Saya lebih suka xargs. Keduanya pada dasarnya tampaknya melakukan keduanya, sementara xargs memiliki lebih banyak opsi, seperti berjalan secara paralel
Johannes Schaub - litb
2
Adam, tidak, '+' itu akan mengumpulkan nama file sebanyak mungkin dan kemudian mengeksekusi. tetapi tidak akan memiliki fungsi rapi seperti berjalan secara paralel :)
Johannes Schaub - litb
2
Perhatikan bahwa jika Anda ingin melakukan sesuatu dengan nama file, Anda harus mengutipnya. Misalnya:find . -type d | while read file; do ls "$file"; done
David Moles
23

Berikut adalah solusi sederhana yang menangani tab dan / atau spasi di nama file. Jika Anda harus berurusan dengan karakter aneh lainnya di nama file seperti baris baru, pilih jawaban lain.

Direktori pengujian

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Kode untuk masuk ke direktori

find test -type d | while read f ; do
  echo "$f"
done

Nama file harus dikutip ( "$f") jika digunakan sebagai argumen. Tanpa tanda kutip, spasi bertindak sebagai pemisah argumen dan beberapa argumen diberikan ke perintah yang dipanggil.

Dan hasilnya:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
cbliard.dll
sumber
terima kasih, ini berfungsi untuk alias yang saya buat untuk mencantumkan berapa banyak ruang yang digunakan setiap direktori di folder saat ini, itu tersedak beberapa dirs dengan spasi di inkarnasi sebelumnya. Ini berfungsi di zsh, tetapi beberapa jawaban lain tidak:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid
2
Jika Anda menggunakan zsh, Anda juga dapat melakukan ini:alias duc='du -sh *(/)'
cbliard
@cblii Ini masih buggy. Coba jalankan dengan nama file dengan, katakanlah, urutan tab, atau beberapa spasi; Anda akan melihat bahwa itu mengubah salah satu dari mereka menjadi satu spasi, karena Anda tidak mengutip di echo Anda. Dan kemudian ada kasus nama file yang berisi baris baru ...
Charles Duffy
@CharlesDuffy Saya mencoba dengan urutan tab dan banyak spasi. Ini bekerja dengan kutipan. Saya juga mencoba dengan baris baru dan tidak berhasil sama sekali. Saya memperbarui jawabannya sesuai. Terima kasih telah menunjukkan hal ini.
cbliard
1
@cbliard Kanan - menambahkan tanda kutip ke perintah echo Anda adalah apa yang saya maksud. Sedangkan untuk baris baru, Anda dapat membuatnya berfungsi dengan menggunakan find -print0dan IFS='' read -r -d '' f.
Charles Duffy
7

Ini sangat rumit dalam Unix standar, dan sebagian besar solusi melanggar baris baru atau karakter lain. Namun, jika Anda menggunakan kumpulan alat GNU, Anda dapat mengeksploitasi findopsi -print0dan menggunakan xargsopsi yang sesuai -0(minus-nol). Ada dua karakter yang tidak dapat muncul dalam nama file sederhana; itu adalah garis miring dan NUL '\ 0'. Jelas sekali, garis miring muncul di nama jalur, jadi solusi GNU dengan menggunakan NUL '\ 0' untuk menandai akhir dari nama itu cerdik dan sangat mudah.

Jonathan Leffler
sumber
4

Mengapa tidak dimasukkan saja

IFS='\n'

di depan untuk perintah? Ini mengubah pemisah bidang dari <Space> <Tab> <Newline> menjadi hanya <Newline>

oshunluvr.dll
sumber
4
find . -print0|while read -d $'\0' file; do echo "$file"; done
Freakus
sumber
1
-d $'\0'persis sama dengan -d ''- karena bash menggunakan string yang diakhiri NUL, karakter pertama dari string kosong adalah NUL, dan untuk alasan yang sama, NUL tidak dapat direpresentasikan sama sekali di dalam string C.
Charles Duffy
4

saya menggunakan

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Bukankah itu cukup?
Ide diambil dari http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

murpel
sumber
tip bagus: itu sangat membantu untuk opsi osascript baris perintah (OS X AppleScript), di mana spasi membagi argumen menjadi beberapa parameter di mana hanya satu yang dimaksudkan
tim
Tidak, itu tidak cukup. Ini tidak efisien (karena penggunaan yang tidak perlu $(echo ...)), tidak menangani nama file dengan ekspresi glob dengan benar, tidak menangani nama file yang berisi $'\b'atau $ '\ n' karakter dengan benar, dan terlebih lagi mengubah beberapa spasi kosong menjadi karakter spasi tunggal di sisi keluaran karena kutipan salah.
Charles Duffy
4

Jangan menyimpan daftar sebagai string; menyimpannya sebagai array untuk menghindari semua kebingungan pembatas ini. Berikut ini contoh skrip yang akan beroperasi di semua subdirektori pengujian, atau daftar yang disediakan pada baris perintahnya:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Sekarang mari kita coba ini di direktori pengujian dengan satu atau dua kurva dilemparkan:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City
Gordon Davisson
sumber
1
Melihat kembali ini - sebenarnya ada solusi dengan POSIX sh: Anda dapat menggunakan kembali "$@"array, menambahkannya dengan set -- "$@" "$f".
Charles Duffy
4

Anda dapat menggunakan IFS (pemisah bidang internal) untuk sementara menggunakan:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS

menakjubkan di sana
sumber
Mohon berikan penjelasan.
Steve K
IFS menentukan simbol pemisah, maka nama file dengan spasi tidak akan terpotong.
amazingthere
$ IFS = $ OLD_IFS di akhir harus: IFS = $ OLD_IFS
Michel
3

ps jika hanya tentang spasi di input, maka beberapa tanda kutip ganda bekerja dengan lancar untuk saya ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
hardbutnot
sumber
2

Untuk menambah apa yang dikatakan Jonathan : gunakan -print0opsi untuk findsehubungan dengan xargssebagai berikut:

find test/* -type d -print0 | xargs -0 command

Itu akan menjalankan perintah commanddengan argumen yang tepat; direktori dengan spasi di dalamnya akan dikutip dengan benar (artinya, direktori tersebut akan diteruskan sebagai satu argumen).

Adam Rosenfield
sumber
1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

Kode di atas akan mengonversi file .mov menjadi .avi. File .mov berada di folder berbeda dan nama folder juga memiliki spasi . Skrip saya di atas akan mengonversi file .mov ke file .avi di folder yang sama. Saya tidak tahu apakah itu membantu Anda.

Kasus:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Bersulang!

Sony George
sumber
echo "$name" | ...tidak berfungsi jika nameada -n, dan bagaimana perilakunya dengan nama dengan urutan backslash-escape bergantung pada implementasi Anda - POSIX membuat perilaku echodalam kasus itu secara eksplisit tidak ditentukan (sedangkan POSIX yang diperluas XSI membuat perluasan urutan backslash-escape yang ditentukan standar perilaku , dan sistem GNU - termasuk bash - tanpa POSIXLY_CORRECT=1melanggar standar POSIX dengan mengimplementasikan -e(sedangkan spesifikasi membutuhkan echo -euntuk mencetak -epada keluaran). printf '%s\n' "$name" | ...lebih aman.
Charles Duffy
1

Harus berurusan dengan spasi putih di nama jalur juga. Apa yang akhirnya saya lakukan adalah menggunakan rekursi dan for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}
Gilles 'SO- berhenti menjadi jahat'
sumber
1
Jangan gunakan functionkata kunci - ini membuat kode Anda tidak kompatibel dengan POSIX sh, tetapi tidak memiliki tujuan berguna lainnya. Anda bisa mendefinisikan sebuah fungsi dengan recursedir() {, menambahkan dua parens dan menghapus kata kunci fungsi, dan ini akan kompatibel dengan semua shell yang sesuai dengan POSIX.
Charles Duffy
1

Ubah daftar file menjadi array Bash. Ini menggunakan pendekatan Matt McClure untuk mengembalikan larik dari fungsi Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Hasilnya adalah cara untuk mengonversi input multi-baris menjadi array Bash.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Pendekatan ini tampaknya berfungsi bahkan ketika ada karakter buruk, dan merupakan cara umum untuk mengonversi input apa pun ke array Bash. Kerugiannya adalah jika inputnya panjang, Anda dapat melebihi batas ukuran baris perintah Bash, atau menggunakan memori dalam jumlah besar.

Pendekatan di mana loop yang pada akhirnya bekerja pada daftar juga memiliki daftar yang disalurkan memiliki kelemahan bahwa membaca stdin tidak mudah (seperti meminta input pengguna), dan loop adalah proses baru sehingga Anda mungkin bertanya-tanya mengapa variabel Anda mengatur di dalam loop tidak tersedia setelah loop selesai.

Saya juga tidak suka pengaturan IFS, itu dapat mengacaukan kode lain.

Steve Zobell
sumber
Jika Anda menggunakan IFS='' read, pada baris yang sama, pengaturan IFS hanya ada untuk perintah baca, dan tidak menghindarinya. Tidak ada alasan untuk tidak menyukai pengaturan IFS dengan cara ini.
Charles Duffy
1

Ya, saya melihat terlalu banyak jawaban yang rumit. Saya tidak ingin melewatkan output dari utilitas find atau menulis loop, karena find memiliki opsi "exec" untuk ini.

Masalah saya adalah saya ingin memindahkan semua file dengan ekstensi dbf ke folder saat ini dan beberapa di antaranya berisi ruang putih.

Saya mengatasinya begitu:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Terlihat sangat sederhana bagi saya

Tebe
sumber
0

baru saja menemukan ada beberapa kesamaan antara pertanyaan saya dan pertanyaan Anda. Aparrently jika Anda ingin meneruskan argumen ke dalam perintah

test.sh "Cherry Hill" "New York City"

untuk mencetaknya secara berurutan

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

perhatikan $ @ dikelilingi oleh tanda kutip ganda, beberapa catatan di sini

Jeffrey04
sumber
0

Saya membutuhkan konsep yang sama untuk memampatkan beberapa direktori atau file secara berurutan dari folder tertentu. Saya telah memecahkan menggunakan awk untuk mem-parsel daftar dari ls dan untuk menghindari masalah ruang kosong di nama.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

Bagaimana menurut anda?

Hìr0
sumber
Saya pikir ini tidak akan berfungsi dengan benar jika nama file memiliki baris baru di dalamnya. Mungkin Anda harus mencobanya.
pengguna000001
0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Johan Kasselman
sumber
-3

Bagi saya ini berhasil, dan cukup "bersih":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done
AndrzejP
sumber
4
Tapi ini lebih buruk. Tanda kutip ganda di sekitar pencarian menyebabkan semua nama jalur digabungkan sebagai string tunggal. Ubah gema menjadi ls untuk melihat masalahnya.
NVRAM
-4

Baru saja mengalami masalah varian sederhana ... Konversikan file dari .flv yang diketik ke .mp3 (menguap).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

menemukan secara rekursif semua file flash pengguna Macintosh dan mengubahnya menjadi audio (salin, tanpa transcode) ... seperti yang disebutkan di atas, mencatat bahwa membaca dan bukan hanya 'untuk file masuk ' akan keluar.

mark washeim
sumber
2
The readsetelah insatu kata lebih dalam daftar Anda iterasi. Apa yang Anda posting adalah versi yang sedikit rusak dari apa yang dimiliki penanya, yang tidak berfungsi. Anda mungkin bermaksud untuk memposting sesuatu yang berbeda, tetapi mungkin saja itu tercakup oleh jawaban lain di sini.
Gilles 'SO- berhenti menjadi jahat'