Bash array dengan spasi di elemen

150

Saya mencoba membuat array di bash dari nama file dari kamera saya:

FILES=(2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg)

Seperti yang Anda lihat, ada ruang di tengah setiap nama file.

Saya sudah mencoba membungkus setiap nama dengan tanda kutip, dan melarikan diri dari ruang dengan backslash, tidak ada yang berhasil.

Ketika saya mencoba mengakses elemen array, itu terus memperlakukan ruang sebagai elemen pendahulunya.

Bagaimana saya bisa menangkap nama file dengan benar dengan spasi di dalam nama?

kasar
sumber
Sudahkah Anda mencoba menambahkan file dengan cara lama? Suka FILES[0] = ...? (Sunting: Saya baru saja melakukannya; tidak berfungsi. Menarik).
Dan Fego
POSIX: stackoverflow.com/questions/2936922/…
Ciro Santilli 郝海东 冠状 病 六四 六四 事件 法轮功
Semua jawaban di sini dirinci untuk saya menggunakan Cygwin. Itu melakukan hal-hal aneh jika ada spasi dalam nama file, titik. Saya mengatasinya dengan membuat "array" dalam daftar file teks dari semua elemen yang ingin saya kerjakan, dan mengulangi beberapa baris dalam file: Pemformatan sedang dilakukan dengan backticks yang dimaksudkan di sekitar perintah dalam kurung: IFS = ""; array = ( find . -maxdepth 1 -type f -iname \*.$1 -printf '%f\n'); untuk elemen dalam $ {array [@]}; lakukan echo $ element; selesai
Alex Hall

Jawaban:

122

Saya pikir masalahnya mungkin sebagian dengan bagaimana Anda mengakses elemen. Jika saya melakukan yang sederhana for elem in $FILES, saya mengalami masalah yang sama seperti Anda. Namun, jika saya mengakses array melalui indeksnya, seperti itu, ia berfungsi jika saya menambahkan elemen secara numerik atau dengan escapes:

for ((i = 0; i < ${#FILES[@]}; i++))
do
    echo "${FILES[$i]}"
done

Salah satu dari deklarasi ini $FILESharus bekerja:

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)

atau

FILES=("2011-09-04 21.43.02.jpg"
"2011-09-05 10.23.14.jpg"
"2011-09-09 12.31.16.jpg"
"2011-09-11 08.43.12.jpg")

atau

FILES[0]="2011-09-04 21.43.02.jpg"
FILES[1]="2011-09-05 10.23.14.jpg"
FILES[2]="2011-09-09 12.31.16.jpg"
FILES[3]="2011-09-11 08.43.12.jpg"
Dan Fego
sumber
6
Perhatikan bahwa Anda harus menggunakan tanda kutip ganda ketika Anda menggunakan elemen array (misalnya echo "${FILES[$i]}"). Tidak masalah untuk echo, tetapi akan untuk apa pun yang menggunakannya sebagai nama file.
Gordon Davisson
26
Tidak perlu untuk mengulang indeks ketika Anda bisa mengulang elemen for f in "${FILES[@]}".
Mark Edgar
10
@ MarkEdgar saya mengalami masalah dengan untuk f di $ {FILES [@]} ketika anggota array memiliki spasi. Tampaknya seluruh array ditafsirkan kembali, dengan spasi yang meludah anggota Anda yang ada menjadi dua atau lebih elemen. Tampaknya "" sangat penting
Michael Shaw
1
Apa yang dilakukan oleh #simbol tajam ( ) dalam for ((i = 0; i < ${#FILES[@]}; i++))pernyataan?
Michal Vician
4
Saya menjawab ini enam tahun yang lalu tapi saya percaya itu untuk mendapatkan hitungan dari jumlah elemen dalam FILES larik.
Dan Fego
91

Pasti ada yang salah dengan cara Anda mengakses item array. Begini cara melakukannya:

for elem in "${files[@]}"
...

Dari halaman bash :

Elemen array dapat dirujuk menggunakan $ {name [subscript]}. ... Jika subskrip adalah @ atau *, kata tersebut diperluas ke semua anggota nama. Subskrip ini berbeda hanya ketika kata itu muncul dalam tanda kutip ganda. Jika kata tersebut dikutip dua kali lipat, $ {name [*]} berkembang menjadi satu kata dengan nilai setiap anggota array dipisahkan oleh karakter pertama dari variabel khusus IFS, dan $ {name [@]} memperluas setiap elemen dari nama ke kata yang terpisah .

Tentu saja, Anda juga harus menggunakan tanda kutip ganda ketika mengakses satu anggota

cp "${files[0]}" /tmp
pengguna123444555621
sumber
3
Solusi paling bersih dan paling elegan dalam kelompok ini, meskipun harus mengulangi kembali bahwa setiap elemen yang didefinisikan dalam array harus dikutip.
maverick
Sementara jawaban Dan Fego efektif, ini adalah cara yang lebih idiomatis untuk menangani ruang dalam elemen.
Daniel Zhang
3
Berasal dari bahasa pemrograman lain, terminologi dari kutipan itu sangat sulit untuk dipahami. Ditambah sintaks yang membingungkan. Saya akan sangat berterima kasih jika Anda bisa membahasnya lebih dalam? Khususnyaexpands to a single word with the value of each array member separated by the first character of the IFS special variable
CL22
1
Ya, setuju bahwa tanda kutip ganda menyelesaikannya dan ini lebih baik daripada solusi lain. Untuk menjelaskan lebih lanjut - kebanyakan yang lain hanya kekurangan tanda kutip ganda. Anda mendapatkan yang benar:, for elem in "${files[@]}"sementara mereka memiliki for elem in ${files[@]}- sehingga ruang membingungkan ekspansi dan untuk mencoba berjalan pada kata-kata individual.
arntg
Ini tidak berfungsi untuk saya di macOS 10.14.4, yang menggunakan "GNU bash, versi 3.2.57 (1) -release (x86_64-apple-darwin18)". Mungkin bug di versi bash yang lebih lama?
Mark Ribau
43

Anda perlu menggunakan IFS untuk menghentikan ruang sebagai pembatas elemen.

FILES=("2011-09-04 21.43.02.jpg"
       "2011-09-05 10.23.14.jpg"
       "2011-09-09 12.31.16.jpg"
       "2011-09-11 08.43.12.jpg")
IFS=""
for jpg in ${FILES[*]}
do
    echo "${jpg}"
done

Jika Anda ingin memisahkan berdasarkan. maka lakukan saja IFS = "." Semoga ini bisa membantu Anda :)

Khushneet
sumber
3
Saya harus memindahkan IFS = "" ke sebelum penugasan array tetapi ini adalah jawaban yang benar.
merampok
Saya menggunakan beberapa array untuk mengurai informasi dan saya akan memiliki efek IFS = "" bekerja hanya di salah satu dari mereka. Setelah saya menggunakan IFS = "" semua array lainnya berhenti parsing sesuai. Ada petunjuk tentang ini?
Paulo Pedroso
Paulo, lihat jawaban lain di sini yang mungkin lebih baik untuk kasus Anda: stackoverflow.com/a/9089186/1041319 . Belum mencoba IFS = "", dan tampaknya itu menyelesaikannya dengan elegan - tetapi contoh Anda menunjukkan mengapa seseorang dapat mengalami masalah dalam beberapa kasus. Dimungkinkan untuk mengatur IFS = "" pada satu baris, tetapi mungkin masih lebih membingungkan daripada solusi lainnya.
arntg
Ini juga bekerja untuk saya di pesta. Terima kasih @Khushneet saya mencari itu selama setengah jam ...
csonuryilmaz
Hebat, hanya jawaban di halaman ini yang berfungsi. Tapi saya juga harus pindah IFS="" sebelum konstruksi array .
pkamb
13

Saya setuju dengan orang lain bahwa kemungkinan Anda mengakses elemen yang menjadi masalahnya. Mengutip nama file dalam penugasan array sudah benar:

FILES=(
  "2011-09-04 21.43.02.jpg"
  "2011-09-05 10.23.14.jpg"
  "2011-09-09 12.31.16.jpg"
  "2011-09-11 08.43.12.jpg"
)

for f in "${FILES[@]}"
do
  echo "$f"
done

Menggunakan tanda kutip ganda di sekitar array mana pun dari bentuk "${FILES[@]}"membagi array menjadi satu kata per elemen array. Tidak ada pemisahan kata selain itu.

Menggunakan "${FILES[*]}"juga memiliki arti khusus, tetapi ia menggabungkan elemen array dengan karakter pertama $ IFS, menghasilkan satu kata, yang mungkin bukan yang Anda inginkan.

Menggunakan bare ${array[@]}atau ${array[*]}subjek hasil ekspansi itu untuk pemecah kata lebih lanjut, sehingga Anda akan berakhir dengan kata-kata terpecah pada spasi (dan apa pun di $IFSdalamnya) alih-alih satu kata per elemen array.

Menggunakan C-style untuk loop juga baik dan menghindari mengkhawatirkan pemisahan kata jika Anda tidak jelas tentang itu:

for (( i = 0; i < ${#FILES[@]}; i++ ))
do
  echo "${FILES[$i]}"
done
Dean Hall
sumber
3

Melarikan diri berhasil.

#!/bin/bash

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)

echo ${FILES[0]}
echo ${FILES[1]}
echo ${FILES[2]}
echo ${FILES[3]}

Keluaran:

$ ./test.sh
2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg

Mengutip string juga menghasilkan output yang sama.

Chris Seymour
sumber
3

Jika Anda memiliki array seperti ini: #! / Bin / bash

Unix[0]='Debian'
Unix[1]="Red Hat"
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${Unix[@]});
    do echo $i;
done

Anda akan mendapatkan:

Debian
Red
Hat
Ubuntu
Suse

Saya tidak tahu mengapa tetapi loop memecah spasi dan menempatkannya sebagai item individual, bahkan Anda mengelilinginya dengan tanda kutip.

Untuk menyiasatinya, alih-alih memanggil elemen dalam array, Anda memanggil indeks, yang mengambil string penuh yang dibungkus dengan tanda kutip. Itu harus dibungkus dengan tanda kutip!

#!/bin/bash

Unix[0]='Debian'
Unix[1]='Red Hat'
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${!Unix[@]});
    do echo ${Unix[$i]};
done

Maka Anda akan mendapatkan:

Debian
Red Hat
Ubuntu
Suse
Jonni2016aa
sumber
2

Bukan jawaban yang tepat untuk masalah kutipan / lolos dari pertanyaan awal, tetapi mungkin sesuatu yang sebenarnya akan lebih berguna untuk op:

unset FILES
for f in 2011-*.jpg; do FILES+=("$f"); done
echo "${FILES[@]}"

Dimana tentu saja ungkapan tersebut harus diadopsi dengan persyaratan khusus (misalnya *.jpguntuk semua atau 2001-09-11*.jpghanya untuk gambar-gambar pada hari tertentu).

TNT
sumber
0

Solusi lain menggunakan loop "while" sebagai ganti loop "for":

index=0
while [ ${index} -lt ${#Array[@]} ]
  do
     echo ${Array[${index}]}
     index=$(( $index + 1 ))
  done
Javier Salas
sumber
0

Jika Anda tidak terjebak untuk menggunakan bash, penanganan spasi yang berbeda dalam nama file adalah salah satu manfaat dari kulit ikan . Pertimbangkan direktori yang berisi dua file: "a b.txt" dan "b c.txt". Berikut adalah perkiraan yang masuk akal dalam memproses daftar file yang dihasilkan dari perintah lain bash, tetapi gagal karena spasi dalam nama file yang Anda alami:

# bash
$ for f in $(ls *.txt); { echo $f; }
a
b.txt
b
c.txt

Dengan fish, sintaks hampir identik, tetapi hasilnya adalah apa yang Anda harapkan:

# fish
for f in (ls *.txt); echo $f; end
a b.txt
b c.txt

Ini bekerja secara berbeda karena ikan membagi output perintah pada baris baru, bukan spasi.

Jika Anda memiliki kasus di mana Anda ingin membagi pada spasi alih-alih baris baru, fishmemiliki sintaks yang sangat mudah dibaca untuk itu:

for f in (ls *.txt | string split " "); echo $f; end
Mark Stosberg
sumber
0

Saya biasa mereset nilai IFS dan rollback setelah selesai.

# backup IFS value
O_IFS=$IFS

# reset IFS value
IFS=""

FILES=(
"2011-09-04 21.43.02.jpg"
"2011-09-05 10.23.14.jpg"
"2011-09-09 12.31.16.jpg"
"2011-09-11 08.43.12.jpg"
)

for file in ${FILES[@]}; do
    echo ${file}
done

# rollback IFS value
IFS=${O_IFS}

Output yang mungkin dari loop:

2011-09-04 21.43.02.jpg

2011-09-05 10.23.14.jpg

2011-09-09 12.31.16.jpg

2011-09-11 08.43.12.jpg

Madan Sapkota
sumber