Mengapa skrip shell saya tercekik di spasi putih atau karakter khusus lainnya?


284

Atau, panduan pengantar untuk penanganan nama file yang kuat dan string lain yang melewati skrip shell.

Saya menulis skrip shell yang berfungsi dengan baik sebagian besar waktu. Tetapi tersedak pada beberapa input (misalnya pada beberapa nama file).

Saya mengalami masalah seperti berikut:

  • Saya memiliki nama file yang mengandung spasi hello world, dan itu diperlakukan sebagai dua file terpisah hellodan world.
  • Saya memiliki jalur input dengan dua spasi berturut-turut dan mereka menyusut menjadi satu di input.
  • Memimpin dan mengikuti spasi menghilang dari jalur input.
  • Terkadang, ketika input berisi salah satu karakter \[*?, mereka digantikan oleh beberapa teks yang sebenarnya adalah nama file.
  • Ada tanda kutip '(atau kutipan ganda ") pada input dan hal-hal menjadi aneh setelah titik itu.
  • Ada backslash dalam input (atau: Saya menggunakan Cygwin dan beberapa nama file saya memiliki \pemisah gaya Windows ).

Apa yang sedang terjadi dan bagaimana cara memperbaikinya?


16
shellcheckmembantu Anda meningkatkan kualitas program Anda.
aurelien

3
Selain teknik perlindungan yang dijelaskan dalam jawaban, dan meskipun mungkin jelas bagi sebagian besar pembaca, saya pikir mungkin ada baiknya berkomentar bahwa ketika file dimaksudkan untuk diproses menggunakan alat baris perintah, adalah praktik yang baik untuk menghindari karakter mewah di nama di tempat pertama, jika memungkinkan.
bli


1
@ Bli Tidak, itu hanya membuat bug membutuhkan waktu lebih lama untuk muncul. Itu menyembunyikan bug hari ini. Dan sekarang, Anda tidak tahu semua nama file yang nantinya digunakan dengan kode Anda.
Volker Siegel

Pertama, jika parameter Anda berisi spasi maka harus dikutip masuk (pada baris perintah). Namun Anda bisa mengambil seluruh baris perintah dan menguraikannya sendiri. Dua ruang tidak berubah menjadi satu ruang; berapapun ruang memberi tahu skrip Anda bahwa itu adalah variabel berikutnya jadi jika Anda melakukan sesuatu seperti "echo $ 1 $ 2" itu adalah skrip Anda yang menempatkan satu spasi di antaranya. Juga gunakan "find (-exec)" untuk beralih di atas file dengan spasi daripada untuk loop; Anda dapat menangani ruang dengan lebih mudah.
Patrick Taylor

Jawaban:


352

Selalu gunakan tanda kutip ganda sekitar substitusi variabel dan substitusi perintah: "$foo","$(foo)"

Jika Anda menggunakan tanda $fookutip, skrip Anda akan tersedak input atau parameter (atau perintah output, dengan $(foo)) yang mengandung spasi atau \[*?.

Di sana, Anda bisa berhenti membaca. Baiklah, ini beberapa lagi:

  • read- Untuk membaca input baris demi baris dengan readbuiltin, gunakanwhile IFS= read -r line; do …
    Plain readtreats backslash dan whitespace khusus.
  • xargs- Hindarixargs . Jika Anda harus menggunakan xargs, buatlah itu xargs -0. Alih-alih find … | xargs, lebih sukafind … -exec … .
    xargsmemperlakukan spasi dan karakter \"'khusus.

Jawaban ini berlaku untuk Bourne / POSIX-gaya kerang ( sh, ash, dash, bash, ksh, mksh, yash...). Pengguna Zsh harus melewatkannya dan membaca bagian akhir Kapan perlu mengutip ganda? sebagai gantinya. Jika Anda ingin seluruh seluk beluk, baca standar atau manual shell Anda.


Perhatikan bahwa penjelasan di bawah ini berisi beberapa perkiraan (pernyataan yang benar di sebagian besar kondisi tetapi dapat dipengaruhi oleh konteks sekitarnya atau oleh konfigurasi).

Mengapa saya harus menulis "$foo"? Apa yang terjadi tanpa tanda kutip?

$footidak berarti "mengambil nilai variabel foo". Itu berarti sesuatu yang jauh lebih kompleks:

  • Pertama, ambil nilai variabel.
  • Pemisahan bidang: memperlakukan nilai itu sebagai daftar bidang yang dipisahkan spasi, dan buat daftar yang dihasilkan. Misalnya, jika variabel berisi foo * bar ​maka hasil dari langkah ini adalah daftar 3-elemen foo, *, bar.
  • Pembuatan nama file: perlakukan setiap bidang sebagai bola, yaitu sebagai pola wildcard, dan gantilah dengan daftar nama file yang cocok dengan pola ini. Jika polanya tidak cocok dengan file apa pun, maka dibiarkan tidak dimodifikasi. Dalam contoh kami, ini menghasilkan daftar yang berisi foo, diikuti oleh daftar file di direktori saat ini, dan akhirnya bar. Jika direktori saat kosong, hasilnya adalah foo, *, bar.

Perhatikan bahwa hasilnya adalah daftar string. Ada dua konteks dalam sintaksis shell: konteks daftar dan konteks string. Pemisahan bidang dan pembuatan nama file hanya terjadi dalam konteks daftar, tapi itu sebagian besar waktu. Kutipan ganda membatasi konteks string: seluruh string yang dikutip ganda adalah string tunggal, bukan untuk dibagi. (Pengecualian: "$@"untuk memperluas ke daftar parameter posisi, misalnya "$@"setara dengan "$1" "$2" "$3"jika ada tiga parameter posisi. Lihat Apa perbedaan antara $ * dan $ @? )

Hal yang sama terjadi pada perintah substitusi dengan $(foo)atau dengan `foo`. Sebagai tambahan, jangan gunakan `foo`: aturan kutipnya aneh dan tidak portabel, dan semua dukungan modern shells $(foo)yang benar-benar setara kecuali memiliki aturan kutipan intuitif.

Output substitusi aritmatika juga mengalami ekspansi yang sama, tetapi itu biasanya tidak menjadi perhatian karena hanya berisi karakter yang tidak dapat diperluas (dengan asumsi IFStidak mengandung angka atau -).

Lihat Kapan perlu kutip ganda? untuk perincian lebih lanjut tentang kasus-kasus ketika Anda dapat meninggalkan tanda kutip.

Kecuali Anda bermaksud agar semua omong kosong ini terjadi, ingatlah untuk selalu menggunakan tanda kutip ganda di sekitar penggantian variabel dan perintah. Berhati-hatilah: meninggalkan tanda kutip tidak hanya mengarah pada kesalahan tetapi juga celah keamanan .

Bagaimana cara saya memproses daftar nama file?

Jika Anda menulis myfiles="file1 file2", dengan spasi untuk memisahkan file, ini tidak dapat berfungsi dengan nama file yang mengandung spasi. Nama file Unix dapat berisi karakter selain /(yang selalu merupakan pemisah direktori) dan null byte (yang tidak dapat Anda gunakan dalam skrip shell dengan sebagian besar shell).

Masalah yang sama dengan myfiles=*.txt; … process $myfiles. Ketika Anda melakukan ini, variabel myfilesberisi string 5-karakter *.txt, dan ketika Anda menulis $myfilesbahwa wildcard diperluas. Contoh ini sebenarnya akan berfungsi, sampai Anda mengubah skrip menjadi myfiles="$someprefix*.txt"; … process $myfiles. Jika someprefixdiatur ke final report, ini tidak akan berhasil.

Untuk memproses daftar apa pun (seperti nama file), masukkan ke dalam array. Ini membutuhkan mksh, ksh93, yash atau bash (atau zsh, yang tidak memiliki semua masalah penawaran ini); shell POSIX biasa (seperti abu atau tanda hubung) tidak memiliki variabel array.

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88 memiliki variabel array dengan sintaks tugas yang berbeda set -A myfiles "someprefix"*.txt(lihat variabel penetapan di bawah lingkungan ksh yang berbeda jika Anda memerlukan portabilitas ksh88 / bash). Shell Bourne / POSIX-style memiliki satu larik tunggal, larik parameter posisional "$@"yang Anda atur setdan yang bersifat lokal untuk suatu fungsi:

set -- "$someprefix"*.txt
process -- "$@"

Bagaimana dengan nama file yang dimulai dengan -?

Pada catatan terkait, perlu diingat bahwa nama file dapat dimulai dengan -(tanda hubung / minus), yang ditafsirkan sebagian besar perintah sebagai menunjukkan opsi. Jika Anda memiliki nama file yang dimulai dengan bagian variabel, pastikan untuk meneruskannya --sebelumnya, seperti dalam cuplikan di atas. Ini menunjukkan perintah bahwa ia telah mencapai akhir opsi, jadi apa pun setelah itu adalah nama file bahkan jika dimulai dengan -.

Atau, Anda dapat memastikan bahwa nama file Anda dimulai dengan karakter selain -. Nama file absolut dimulai dengan /, dan Anda dapat menambahkan ./di awal nama relatif. Cuplikan berikut mengubah konten variabel fmenjadi cara "aman" untuk merujuk ke file yang sama yang dijamin tidak akan memulai -.

case "$f" in -*) "f=./$f";; esac

Pada catatan akhir tentang topik ini, berhati-hatilah karena beberapa perintah menafsirkan -sebagai input standar atau output standar, bahkan setelahnya --. Jika Anda perlu merujuk ke file yang sebenarnya bernama -, atau jika Anda memanggil program seperti itu dan Anda tidak ingin itu membaca dari stdin atau menulis ke stdout, pastikan untuk menulis ulang -seperti di atas. Lihat Apa perbedaan antara "du -sh *" dan "du -sh ./*"? untuk diskusi lebih lanjut.

Bagaimana cara menyimpan perintah dalam variabel?

"Command" dapat berarti tiga hal: nama perintah (nama sebagai executable, dengan atau tanpa path lengkap, atau nama fungsi, builtin atau alias), nama perintah dengan argumen, atau sepotong kode shell. Ada berbagai cara menyimpannya dalam suatu variabel.

Jika Anda memiliki nama perintah, simpan saja dan gunakan variabel dengan tanda kutip ganda seperti biasa.

command_path="$1"

"$command_path" --option --message="hello world"

Jika Anda memiliki perintah dengan argumen, masalahnya sama dengan daftar nama file di atas: ini adalah daftar string, bukan string. Anda tidak bisa hanya memasukkan argumen ke dalam string tunggal dengan spasi di antaranya, karena jika Anda melakukannya, Anda tidak bisa membedakan antara spasi yang merupakan bagian dari argumen dan spasi yang memisahkan argumen. Jika shell Anda memiliki array, Anda dapat menggunakannya.

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

Bagaimana jika Anda menggunakan shell tanpa array? Anda masih dapat menggunakan parameter posisi, jika Anda tidak keberatan memodifikasinya.

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

Bagaimana jika Anda perlu menyimpan perintah shell yang kompleks, misalnya dengan pengalihan, pipa, dll? Atau jika Anda tidak ingin mengubah parameter posisi? Kemudian Anda bisa membuat string yang berisi perintah, dan menggunakan evalbuiltin.

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

Perhatikan tanda kutip tersarang dalam definisi code: tanda kutip tunggal '…'membatasi string literal, sehingga nilai variabel codeadalah string /path/to/executable --option --message="hello world" -- /path/to/file1. The evalbuiltin memberitahu shell untuk mengurai string dilewatkan sebagai argumen seolah-olah itu muncul di script, sehingga pada saat itu tanda kutip dan pipa diurai, dll

Penggunaan evalitu sulit. Pikirkan baik-baik tentang apa yang diuraikan kapan. Khususnya, Anda tidak bisa begitu saja memasukkan nama file ke dalam kode: Anda perlu mengutipnya, sama seperti yang akan Anda lakukan jika berada dalam file kode sumber. Tidak ada cara langsung untuk melakukan itu. Sesuatu seperti code="$code $filename"istirahat jika nama file mengandung karakter khusus shell (spasi, $, ;, |, <, >, dll). code="$code \"$filename\""masih istirahat "$\`. Bahkan code="$code '$filename'"pecah jika nama file berisi a '. Ada dua solusi.

  • Tambahkan lapisan tanda kutip di sekitar nama file. Cara termudah untuk melakukannya adalah dengan menambahkan tanda kutip tunggal di sekitarnya, dan mengganti tanda kutip tunggal dengan '\''.

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
  • Simpan ekspansi variabel di dalam kode, sehingga terlihat ketika kode dievaluasi, bukan ketika fragmen kode dibangun. Ini lebih sederhana tetapi hanya berfungsi jika variabel masih ada dengan nilai yang sama pada saat kode dieksekusi, bukan misalnya jika kode dibangun dalam satu lingkaran.

    code="$code \"\$filename\""

Akhirnya, apakah Anda benar-benar membutuhkan variabel yang berisi kode? Cara paling alami untuk memberi nama pada blok kode adalah dengan mendefinisikan suatu fungsi.

Ada apa dengan ini read?

Tanpa -r, readmemungkinkan jalur lanjutan - ini adalah satu jalur input logis:

hello \
world

readmemisahkan jalur input ke dalam bidang yang dibatasi oleh karakter di $IFS(tanpa -r, garis miring terbalik juga lolos dari karakter). Misalnya, jika inputnya berupa baris yang berisi tiga kata, maka read first second thirdset firstke kata input pertama, secondke kata kedua dan thirdke kata ketiga. Jika ada lebih banyak kata, variabel terakhir berisi semua yang tersisa setelah mengatur yang sebelumnya. Ruang putih terkemuka dan trailing dipangkas.

Pengaturan IFSke string kosong menghindari pemangkasan apa pun. Lihat Mengapa `sementara IFS = read` sering digunakan, alih-alih` IFS =; saat membaca..`? untuk penjelasan yang lebih panjang.

Ada apa dengan ini xargs?

Format input dari xargsstring yang dipisahkan spasi-putih yang secara opsional dapat dikutip tunggal atau ganda. Tidak ada alat standar yang menghasilkan format ini.

Input ke xargs -L1atau xargs -lhampir merupakan daftar baris, tetapi tidak cukup - jika ada spasi di akhir baris, baris berikut adalah garis lanjutan.

Anda dapat menggunakan xargs -0mana yang berlaku (dan jika tersedia: GNU (Linux, Cygwin), BusyBox, BSD, OSX, tetapi tidak dalam POSIX). Itu aman, karena byte nol tidak dapat muncul di sebagian besar data, khususnya dalam nama file. Untuk menghasilkan daftar nama file yang dipisahkan nol, gunakan find … -print0(atau Anda dapat menggunakan find … -exec …seperti yang dijelaskan di bawah).

Bagaimana cara saya memproses file yang ditemukan oleh find?

find  -exec some_command a_parameter another_parameter {} +

some_commandharus berupa perintah eksternal, tidak boleh berupa fungsi shell atau alias. Jika Anda perlu meminta shell untuk memproses file, panggil shsecara eksplisit.

find  -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

Saya punya pertanyaan lain

Jelajahi tag di situs ini, atau atau . (Klik "pelajari lebih lanjut ..." untuk melihat beberapa kiat umum dan daftar pertanyaan umum pilihan tangan.) Jika Anda telah mencari dan Anda tidak dapat menemukan jawabannya, tanyakan .


6
@ John1024 Ini hanya fitur GNU, jadi saya akan tetap menggunakan "no tool standar".
Gilles

2
Anda juga perlu mengutip sekitar $(( ... ))(juga $[...]dalam beberapa shell) kecuali dalam zsh(bahkan dalam emulasi sh) dan mksh.
Stéphane Chazelas

3
Perhatikan bahwa xargs -0ini bukan POSIX. Kecuali dengan FreeBSD xargs, Anda umumnya ingin xargs -r0bukan xargs -0.
Stéphane Chazelas

2
@ John1024, tidak, ls --quoting-style=shell-alwaystidak kompatibel dengan xargs. Cobatouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas

3
Fitur bagus lainnya (khusus GNU) adalah xargs -d "\n"agar Anda dapat menjalankan mis locate PATTERN1 |xargs -d "\n" grep PATTERN2untuk mencari nama file yang cocok dengan PATTERN1 dengan konten yang cocok dengan PATTERN2 . Tanpa GNU, Anda dapat melakukannya misalnyalocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Adam Katz

26

Sementara jawaban Gilles sangat bagus, saya mengambil masalah pada poin utamanya

Selalu gunakan tanda kutip ganda di sekitar substitusi variabel dan substitusi perintah: "$ foo", "$ (foo)"

Ketika Anda memulai dengan shell mirip Bash yang melakukan pemisahan kata, ya tentu saja saran yang aman adalah selalu menggunakan tanda kutip. Namun pemisahan kata tidak selalu dilakukan

§ Pemisahan Kata

Perintah-perintah ini dapat dijalankan tanpa kesalahan

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

Saya tidak mendorong pengguna untuk mengadopsi perilaku ini, tetapi jika seseorang benar-benar memahami kapan pemisahan kata terjadi maka mereka harus dapat memutuskan sendiri kapan harus menggunakan tanda kutip.


19
Seperti yang saya sebutkan dalam jawaban saya, lihat unix.stackexchange.com/questions/68694/… untuk detailnya. Perhatikan pertanyaan - "Mengapa skrip shell saya tersedak?". Masalah yang paling umum (dari pengalaman bertahun-tahun di situs ini dan di tempat lain) tidak ada tanda kutip ganda. "Selalu gunakan tanda kutip ganda" lebih mudah diingat daripada "selalu gunakan tanda kutip ganda, kecuali untuk kasus-kasus ini di mana mereka tidak diperlukan".
Gilles

14
Aturan sulit dipahami untuk pemula. Sebagai contoh, foo=$bartidak apa-apa, tetapi export foo=$baratau env foo=$vartidak (setidaknya dalam beberapa shell). Saran untuk pemula: selalu kutip variabel Anda kecuali Anda tahu apa yang Anda lakukan dan punya alasan kuat untuk tidak melakukannya .
Stéphane Chazelas

5
@ SevenPenny Apakah ini benar-benar lebih benar? Apakah ada kasus yang masuk akal di mana kutipan akan merusak skrip? Dalam situasi di mana dalam setengah kasus kutipan harus digunakan, dan dalam setengah lainnya kutipan dapat digunakan secara opsional - maka rekomendasi "selalu menggunakan kutipan, untuk berjaga-jaga" adalah yang harus dipikirkan, karena itu benar, sederhana dan kurang berisiko. Mengajarkan daftar pengecualian seperti itu kepada pemula diketahui tidak efektif (tidak memiliki konteks, mereka tidak akan mengingatnya) dan kontraproduktif, karena mereka akan mengacaukan kutipan yang diperlukan / tidak dibutuhkan, melanggar skrip mereka dan menurunkan motivasi mereka untuk belajar lebih lanjut.
Peteris

6
$ 0,02 saya adalah merekomendasikan untuk mengutip semuanya adalah saran yang bagus. Mengutip secara keliru mengutip sesuatu yang tidak membutuhkannya tidak berbahaya, keliru gagal mengutip sesuatu yang memang membutuhkannya berbahaya. Jadi, bagi sebagian besar penulis skrip shell yang tidak akan pernah memahami seluk-beluk kapan tepatnya pemisahan kata terjadi, mengutip semuanya jauh lebih aman daripada mencoba mengutip hanya jika diperlukan.
godlygeek

5
@Peteris dan godlygeek: "Apakah ada kasus yang masuk akal di mana kutipan akan merusak skrip?" Itu tergantung pada definisi Anda tentang "masuk akal". Jika skrip ditetapkan criteria="-type f", maka find . $criteriaberfungsi tetapi find . "$criteria"tidak.
G-Man

22

Sejauh yang saya tahu, hanya ada dua kasus di mana perlu untuk melipatgandakan kuotasi ekspansi, dan kasus-kasus itu melibatkan dua parameter shell khusus "$@"dan "$*"- yang ditentukan untuk berkembang secara berbeda ketika diapit dengan tanda kutip ganda. Dalam semua kasus lain (tidak termasuk, mungkin, implementasi array shell-spesifik) perilaku ekspansi adalah hal yang dapat dikonfigurasi - ada opsi untuk itu.

Ini tidak berarti, tentu saja, bahwa kutip ganda harus dihindari - sebaliknya, itu mungkin metode yang paling nyaman dan kuat untuk membatasi ekspansi yang ditawarkan shell. Tapi, saya pikir, karena alternatif telah diuraikan secara ahli, ini adalah tempat yang bagus untuk membahas apa yang terjadi ketika shell mengekspansi nilai.

Shell, dalam hati dan jiwanya (bagi mereka yang memiliki itu) , adalah penerjemah perintah - ia adalah pengurai, seperti yang besar, interaktif sed,. Jika pernyataan shell Anda tersedak pada spasi putih atau serupa, maka sangat mungkin karena Anda belum sepenuhnya memahami proses interpretasi shell - terutama bagaimana dan mengapa ia menerjemahkan pernyataan input ke perintah yang dapat ditindaklanjuti. Tugas shell adalah untuk:

  1. menerima input

  2. menafsirkan dan membaginya dengan benar menjadi kata input tokenized

    • kata input adalah item sintaks shell seperti $wordatauecho $words 3 4* 5

    • kata - kata selalu terpecah pada spasi putih - itu hanya sintaksis - tetapi hanya karakter spasi putih literal yang disajikan ke shell dalam file inputnya

  3. perluas itu jika perlu ke berbagai bidang

    • bidang hasil dari ekspansi kata - mereka membuat perintah yang dapat dieksekusi akhir

    • kecuali "$@", $IFS pemisahan bidang , dan perluasan nama jalur, kata input harus selalu dievaluasi ke satu bidang .

  4. dan kemudian untuk menjalankan perintah yang dihasilkan

    • dalam banyak kasus ini melibatkan pengalihan hasil interpretasinya dalam beberapa bentuk atau lainnya

Orang sering mengatakan cangkang adalah lem , dan, jika ini benar, maka yang ditempelkan adalah daftar argumen - atau bidang - untuk satu proses atau lainnya ketika itu execadalah mereka. Sebagian besar shell tidak menangani NULbyte dengan baik - jika sama sekali - dan ini karena mereka sudah membelahnya. Shell harus exec banyak dan harus melakukan ini dengan NULarray argumen terbatas yang diserahkan ke kernel sistem pada execwaktu. Jika Anda mencampurkan pembatas shell dengan data yang dibatasi maka shell mungkin akan mengacaukannya. Struktur data internal - seperti kebanyakan program - bergantung pada pembatas itu. zsh, terutama, tidak mengacaukannya.

Dan di situlah $IFSmasuk. $IFSAdalah parameter shell yang selalu ada - dan juga dapat disetel - yang menentukan bagaimana shell harus membagi ekspansi shell dari kata ke bidang - khususnya pada nilai apa yang harus dibatasi bidang tersebut. $IFSmembagi ekspansi shell pada pembatas selain NUL- atau, dengan kata lain pengganti shell byte yang dihasilkan dari ekspansi yang cocok dengan nilai dari $IFSdengan NULdata-array internal. Ketika Anda melihatnya seperti itu, Anda mungkin mulai melihat bahwa setiap ekspansi shell field-split adalah $IFSlarik data yang telah direvisi.

Sangat penting untuk memahami bahwa $IFShanya delimits ekspansi yang tidak sudah dinyatakan dibatasi - yang dapat Anda lakukan dengan "tanda kutip ganda. Ketika Anda mengutip suatu ekspansi, Anda membatasinya di kepala dan setidaknya sampai pada nilainya. Dalam kasus $IFStersebut tidak berlaku karena tidak ada bidang yang harus dipisahkan. Bahkan, ekspansi yang dikutip ganda menunjukkan perilaku pemisahan bidang yang identik dengan ekspansi yang tidak dikutip ketika IFS=diatur ke nilai kosong.

Kecuali dikutip, $IFSitu sendiri $IFSekspansi shell terbatas. Ini default ke nilai yang ditentukan <space><tab><newline>- ketiganya menunjukkan properti khusus ketika terkandung di dalamnya $IFS. Sedangkan nilai lain untuk $IFSditentukan untuk mengevaluasi ke satu bidang per kejadian ekspansi , $IFS spasi putih - salah satu dari ketiganya - ditentukan untuk kawin lari ke satu bidang per urutan ekspansi dan urutan terkemuka / trailing dieliminasi seluruhnya. Ini mungkin paling mudah dipahami melalui contoh.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

Tapi itu hanya $IFS- hanya pemisahan kata atau spasi putih seperti yang diminta, jadi bagaimana dengan karakter khusus ?

Shell - secara default - juga akan memperluas token yang tidak dikutip tertentu (seperti yang ?*[disebutkan di tempat lain di sini) menjadi beberapa bidang ketika mereka muncul dalam daftar. Ini disebut ekspansi pathname , atau globbing . Ini adalah alat yang sangat berguna, dan, karena terjadi setelah pemisahan bidang dalam urutan parse shell, itu tidak terpengaruh oleh $ IFS - bidang yang dihasilkan oleh ekspansi pathname dibatasi pada kepala / ekor nama file itu sendiri terlepas dari apakah isinya berisi karakter apa saja yang sedang dalam $IFS. Perilaku ini diaktifkan secara default - tetapi sangat mudah dikonfigurasi sebaliknya.

set -f

Itu menginstruksikan shell untuk tidak glob . Perluasan pathname tidak akan terjadi setidaknya sampai pengaturan itu dibatalkan - seperti jika shell saat ini diganti dengan proses shell baru atau ....

set +f

... dikeluarkan ke shell. Kutipan ganda - seperti yang mereka lakukan untuk $IFS pemisahan lapangan - membuat pengaturan global ini tidak perlu per ekspansi. Begitu:

echo "*" *

... jika perluasan nama jalur diaktifkan saat ini kemungkinan akan menghasilkan hasil yang sangat berbeda per argumen - karena yang pertama hanya akan diperluas ke nilai literalnya (karakter tanda bintang tunggal, yaitu, tidak sama sekali) dan yang kedua hanya untuk yang sama jika direktori kerja saat ini tidak mengandung nama file yang mungkin cocok (dan cocok dengan hampir semua dari mereka) . Namun jika Anda melakukannya:

set -f; echo "*" *

... hasil untuk kedua argumen itu identik - *tidak berkembang dalam kasus itu.


Saya benar-benar setuju dengan @ StéphaneChazelas bahwa itu (kebanyakan) membingungkan hal-hal lebih dari membantu ... tapi saya merasa itu membantu, secara pribadi, jadi saya terbalik. Sekarang saya punya ide yang lebih baik (dan beberapa contoh) tentang bagaimana IFSsebenarnya bekerja. Apa yang saya tidak mengerti adalah mengapa hal itu akan pernah menjadi ide yang baik untuk mengatur IFSuntuk sesuatu selain default.
Wildcard

1
@ Kartu Memori - ini adalah pembatas bidang. jika Anda memiliki nilai dalam variabel yang ingin Anda rentangkan ke beberapa bidang tempat Anda membaginya $IFS. cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; donecetakan \nkemudian usr\nkemudian bin\n. Yang pertama echokosong karena /merupakan bidang nol. Komponen path_components dapat memiliki baris baru atau spasi atau apa pun - tidak masalah karena komponen terpecah /dan bukan nilai default. orang melakukannya awksetiap saat. shell Anda melakukannya juga
mikeserv

3

Saya memiliki proyek video besar dengan spasi dalam nama file dan spasi dalam nama direktori. Sementara find -type f -print0 | xargs -0berfungsi untuk beberapa tujuan dan lintas shell yang berbeda, saya menemukan bahwa menggunakan custom IFS (pemisah bidang input) memberi Anda lebih banyak fleksibilitas jika Anda menggunakan bash. Cuplikan di bawah ini menggunakan bash dan set IFS menjadi hanya baris baru; asalkan tidak ada baris baru di nama file Anda:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

Perhatikan penggunaan parens untuk mengisolasi redefinisi IFS. Saya sudah membaca posting lain tentang cara memulihkan IFS, tetapi ini lebih mudah.

Selain itu, pengaturan IFS ke baris baru memungkinkan Anda mengatur variabel shell sebelumnya dan dengan mudah mencetaknya. Misalnya, saya bisa menumbuhkan variabel V secara bertahap menggunakan baris baru sebagai pemisah:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

dan dengan demikian:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

Sekarang saya bisa "daftar" pengaturan V dengan echo "$V"menggunakan tanda kutip ganda untuk menampilkan baris baru. (Kredit ke utas ini untuk $'\n'penjelasannya.)


3
Tapi kemudian Anda masih akan memiliki masalah dengan nama file yang mengandung karakter baris baru atau glob. Lihat juga: Mengapa mengulangi hasil praktik buruk? . Jika menggunakan zsh, Anda dapat menggunakan IFS=$'\0'dan menggunakan -print0( zshtidak melakukan globbing pada ekspansi sehingga karakter glob tidak menjadi masalah di sana).
Stéphane Chazelas

1
Ini berfungsi dengan nama file yang berisi spasi, tetapi itu tidak bekerja terhadap nama file yang berpotensi bermusuhan atau nama file "tidak masuk akal" yang tidak disengaja. Anda dapat dengan mudah memperbaiki masalah nama file yang mengandung karakter wildcard dengan menambahkan set -f. Di sisi lain, pendekatan Anda pada dasarnya gagal dengan nama file yang berisi baris baru. Saat berurusan dengan data selain nama file, itu juga gagal dengan item kosong.
Gilles

Benar, peringatan saya adalah bahwa itu tidak akan berfungsi dengan baris baru dalam nama file. Namun, saya percaya kita harus menarik garis hanya malu kegilaan ;-)
Russ

Dan saya tidak yakin mengapa ini menerima downvote. Ini adalah metode yang sangat masuk akal untuk mengulangi nama file dengan spasi. Menggunakan -print0 membutuhkan xargs, dan ada hal-hal yang sulit menggunakan rantai itu. Saya minta maaf seseorang tidak setuju dengan jawaban saya, tapi itu bukan alasan untuk menurunkannya.
Russ

0

Mempertimbangkan semua implikasi keamanan yang disebutkan di atas dan dengan asumsi Anda percaya dan memiliki kendali atas variabel yang Anda kembangkan, dimungkinkan untuk memiliki beberapa jalur dengan spasi putih yang digunakan eval. Tetapi berhati-hatilah!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.