Apakah ini pendekatan yang waras untuk "mencadangkan" variabel $ IFS?

19

Saya selalu benar-benar ragu-ragu untuk dipusingkan $IFSkarena hal itu menghancurkan global.

Tetapi seringkali itu membuat memuat string menjadi bash array bagus dan ringkas, dan untuk bash scripting, keringkasan sulit didapat.

Jadi saya pikir mungkin lebih baik daripada tidak sama sekali jika saya mencoba "menyimpan" konten awal dari $IFSvariabel lain dan mengembalikannya segera setelah saya selesai menggunakan $IFSsesuatu.

Apakah ini praktis? Atau pada dasarnya itu tidak ada gunanya dan saya harus langsung IFSkembali ke apa pun yang diperlukan untuk penggunaan selanjutnya?

Steven Lu
sumber
Mengapa itu tidak praktis?
Bratchley
Karena tidak menyetel IFS akan melakukan pekerjaan dengan baik.
llua
1
Bagi mereka yang mengatakan bahwa mengeset IFS akan berfungsi dengan baik, perlu diingat bahwa itu situasional: stackoverflow.com/questions/39545837/… . Dalam pengalaman saya, yang terbaik adalah mengatur IFS secara manual ke default untuk juru bahasa shell Anda, yaitu $' \t\n'jika Anda menggunakan bash. unset $IFStidak selalu mengembalikannya ke apa yang Anda harapkan sebagai default.
Darrel Holt

Jawaban:

9

Anda dapat menyimpan dan menetapkan ke IFS sesuai kebutuhan. Tidak ada yang salah dengan melakukannya. Tidak jarang menyimpan nilainya untuk restorasi setelah modifikasi sementara yang cepat, seperti contoh penetapan array Anda.

Seperti @llua menyebutkan dalam komentarnya untuk pertanyaan Anda, hanya dengan tidak mengaktifkan IFS akan mengembalikan perilaku default, setara dengan menetapkan spasi-tab-baris baru.

Ada baiknya mempertimbangkan bagaimana hal itu bisa lebih bermasalah untuk tidak secara eksplisit mengatur / menghapus IFS daripada melakukannya.

Dari edisi POSIX 2013, 2.5.3 Variabel Shell :

Implementasi dapat mengabaikan nilai IFS di lingkungan, atau tidak adanya IFS dari lingkungan, pada saat shell dipanggil, dalam hal ini shell akan mengatur IFS ke <spasi> <tab> <newline> ketika dipanggil .

Shell yang mematuhi POSIX, dipanggil mungkin atau mungkin tidak mewarisi IFS dari lingkungannya. Dari berikut ini:

  • Skrip portabel tidak dapat secara inheren mewarisi IFS melalui lingkungan.
  • Sebuah skrip yang bermaksud untuk hanya menggunakan perilaku pemisahan standar (atau bergabung, dalam kasus "$*"), tetapi yang dapat berjalan di bawah shell yang menginisialisasi IFS dari lingkungan, harus secara eksplisit mengatur / membatalkan pengaturan IFS untuk mempertahankan diri terhadap intrusi lingkungan.

NB Penting untuk dipahami bahwa untuk diskusi ini kata "dipanggil" memiliki arti tertentu. Sebuah shell dipanggil hanya ketika secara eksplisit dipanggil menggunakan namanya (termasuk #!/path/to/shellshebang). Subshell - seperti yang mungkin dibuat oleh $(...)atau cmd1 || cmd2 &- bukan shell yang dipanggil, dan IFS-nya (bersama dengan sebagian besar lingkungan eksekusinya) identik dengan induknya. Shell yang dipanggil menetapkan nilai $untuk pidnya, sementara subkulit mewarisinya.


Ini bukan semata-mata pembebasan dendam; ada perbedaan nyata dalam bidang ini. Berikut ini adalah skrip singkat yang menguji skenario menggunakan beberapa shell yang berbeda. Ini mengekspor IFS yang dimodifikasi (diatur ke :) ke shell yang dipanggil yang kemudian mencetak IFS default.

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFS umumnya tidak ditandai untuk ekspor, tetapi, jika ya, perhatikan bagaimana bash, ksh93, dan mksh mengabaikan lingkungan mereka IFS=:, sementara dash dan busybox menghormatinya.

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

Beberapa info versi:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

Meskipun bash, ksh93, dan mksh tidak menginisialisasi IFS dari lingkungan, mereka mengekspor kembali IFS yang dimodifikasi.

Jika karena alasan apa pun Anda perlu melewati IFS dengan mudah melalui lingkungan, Anda tidak dapat melakukannya dengan menggunakan IFS itu sendiri; Anda perlu menetapkan nilai ke variabel yang berbeda dan menandai variabel itu untuk ekspor. Anak-anak kemudian perlu secara eksplisit menetapkan nilai itu ke IFS mereka.

Barefoot IO
sumber
Begitu ya, jadi jika saya dapat parafrase, ini bisa dibilang lebih portabel untuk secara eksplisit menentukan IFSnilai dalam sebagian besar situasi di mana itu akan digunakan, dan seringkali tidak produktif untuk mencoba "mempertahankan" nilai aslinya.
Steven Lu
1
Masalah terpenting adalah bahwa jika skrip Anda menggunakan IFS, skrip harus secara eksplisit mengatur / menghapus setel IFS untuk memastikan bahwa nilainya sesuai dengan yang Anda inginkan. Biasanya, perilaku skrip Anda bergantung pada IFS jika ada ekspansi parameter yang tidak readdikutip , penggantian perintah yang tidak dikutip , ekspansi aritmatika yang tidak dikutip , s, atau referensi kutipan ganda $*. Daftar itu berada di luar kepala saya, jadi mungkin tidak komprehensif (terutama ketika mempertimbangkan POSIX-extensions of modern shells).
Barefoot IO
10

Secara umum, ini adalah praktik yang baik untuk mengembalikan kondisi ke default.

Namun, dalam hal ini, tidak terlalu banyak.

Mengapa?:

Juga, menyimpan nilai IFS memiliki masalah.
Jika IFS asli tidak disetel, kode IFS="$OldIFS"akan mengatur IFS ke "", bukan membatalkannya.

Untuk benar-benar menjaga nilai IFS (bahkan jika tidak disetel), gunakan ini:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.
Komunitas
sumber
IFS tidak bisa benar-benar tidak disetel. Jika Anda menghapusnya, shell mengembalikannya ke nilai default. Jadi Anda tidak benar-benar perlu memeriksa itu saat menyimpannya.
filbranden
Berhati-hatilah dalam bash, unset IFSgagal mengeset IFS jika telah dinyatakan lokal dalam konteks induk (konteks fungsi) dan tidak dalam konteks saat ini.
Stéphane Chazelas
5

Anda benar untuk ragu-ragu untuk mengalahkan global. Jangan takut, adalah mungkin untuk menulis kode kerja bersih tanpa pernah memodifikasi global yang sebenarnya IFS, atau melakukan save / restore dance yang rumit dan rentan kesalahan.

Kamu bisa:

  • atur IFS untuk satu permintaan:

    IFS=value command_or_function

    atau

  • mengatur IFS di dalam subkulit:

    (IFS=value; statement)
    $(IFS=value; statement)

Contohnya

  • Untuk mendapatkan string yang dibatasi koma dari array:

    str="$(IFS=, ; echo "${array[*]-}")"

    Catatan: Ini -hanya untuk melindungi array kosong terhadap set -udengan memberikan nilai default saat tidak disetel (nilai tersebut menjadi string kosong dalam kasus ini) .

    The IFSmodifikasi hanya berlaku di dalam subkulit melahirkan dengan $() substitusi perintah . Ini karena subkulit memiliki salinan variabel shell yang memohon dan karenanya dapat membaca nilainya, tetapi setiap modifikasi yang dilakukan oleh subkulit hanya memengaruhi salinan subkulit dan bukan variabel induk.

    Anda mungkin juga berpikir: mengapa tidak melewatkan subshell dan lakukan saja ini:

    IFS=, str="${array[*]-}"  # Don't do this!

    Tidak ada permintaan perintah di sini, dan baris ini malah ditafsirkan sebagai dua penugasan variabel berikutnya yang independen, seolah-olah:

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    Akhirnya, mari kita jelaskan mengapa varian ini tidak akan berfungsi:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    The echoPerintah memang akan disebut dengan nya IFSvariabel set untuk ,, tetapi echotidak peduli atau penggunaan IFS. Keajaiban memperluas "${array[*]}"ke string dilakukan oleh (sub-) shell itu sendiri sebelum echobahkan dipanggil.

  • Untuk membaca seluruh file (yang tidak mengandung NULLbyte) ke dalam satu variabel bernama VAR:

    IFS= read -r -d '' VAR < "${filepath}"

    Catatan: IFS=sama dengan IFS=""dan IFS='', yang semuanya mengatur IFS ke string kosong, yang sangat berbeda dari unset IFS: jika IFStidak disetel, perilaku semua fungsi bash yang digunakan secara internal IFSpersis sama seperti jika IFSmemiliki nilai default $' \t\n'.

    Pengaturan IFSke string kosong memastikan spasi putih memimpin dan tertinggal dipertahankan.

    Perintah -d ''atau -d ""read read hanya menghentikan permintaannya saat ini pada NULLbyte, bukan pada baris baru yang biasa.

  • Untuk membagi $PATHbersama nya :pembatas:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    Contoh ini murni ilustratif. Dalam kasus umum di mana Anda membelah sepanjang pembatas, dimungkinkan untuk masing-masing bidang berisi (versi yang lolos dari) pembatas itu. Bayangkan mencoba membaca dalam satu baris .csvfile yang kolomnya sendiri mungkin berisi koma (lolos atau dikutip dengan cara tertentu). Cuplikan di atas tidak akan berfungsi sebagaimana dimaksud untuk kasus tersebut.

    Yang mengatakan, Anda tidak mungkin menemukan :-containing-path di dalamnya $PATH. Sementara UNIX / Linux pathnames diizinkan mengandung :, tampaknya bash tidak akan mampu menangani jalur seperti itu jika Anda mencoba menambahkannya ke Anda $PATHdan menyimpan file yang dapat dieksekusi di dalamnya, karena tidak ada kode untuk mengurai kolon yang lolos / dikutip titik dua : kode sumber bash 4.4 .

    Akhirnya, perhatikan bahwa snippet menambahkan baris baru ke elemen terakhir dari array yang dihasilkan (sebagaimana dipanggil oleh @ StéphaneChazelas dalam komentar yang sekarang dihapus), dan bahwa jika inputnya adalah string kosong, output akan menjadi elemen tunggal. array, di mana elemen akan terdiri dari baris baru ( $'\n').

Motivasi

old_IFS="${IFS}"; command; IFS="${old_IFS}"Pendekatan dasar yang menyentuh global IFSakan bekerja seperti yang diharapkan untuk skrip yang paling sederhana. Namun, segera setelah Anda menambahkan kerumitan, hal itu dapat dengan mudah pecah dan menyebabkan masalah halus:

  • Jika commandadalah fungsi bash yang juga memodifikasi global IFS(baik secara langsung atau, disembunyikan dari tampilan, di dalam fungsi lain yang ia panggil), dan saat melakukannya secara keliru menggunakan old_IFSvariabel global yang sama untuk melakukan save / restore, Anda mendapatkan bug.
  • Seperti yang ditunjukkan dalam komentar oleh @Gilles ini , jika keadaan awal IFStidak disetel, save-and-restore yang naif tidak akan berfungsi, dan bahkan akan mengakibatkan kegagalan langsung jika opsi shell yang umum (salah) digunakan set -u(alias set -o nounset) digunakan berlaku.
  • Beberapa kode shell dimungkinkan untuk dieksekusi secara tidak sinkron ke aliran eksekusi utama, seperti dengan penangan sinyal (lihat help trap). Jika kode itu juga memodifikasi global IFSatau menganggapnya memiliki nilai tertentu, Anda bisa mendapatkan bug yang halus.

Anda dapat menyusun urutan simpan / kembalikan yang lebih kuat (seperti yang diusulkan dalam jawaban lain ini untuk menghindari beberapa atau semua masalah ini. Namun, Anda harus mengulangi potongan kode boilerplate yang berisik itu di mana pun Anda sementara membutuhkan kustom IFS. Ini mengurangi keterbacaan dan pemeliharaan kode.

Pertimbangan tambahan untuk skrip mirip perpustakaan

IFSsecara khusus menjadi perhatian bagi penulis perpustakaan fungsi shell yang perlu memastikan kode mereka bekerja dengan baik terlepas dari keadaan global ( IFS, opsi shell, ...) yang diberlakukan oleh penjajah mereka, dan juga tanpa mengganggu keadaan itu sama sekali (para penjajah mungkin mengandalkan di atasnya untuk selalu tetap statis).

Saat menulis kode pustaka, Anda tidak dapat mengandalkan IFSmemiliki nilai tertentu (bahkan bukan nilai default) atau bahkan disetel sama sekali. Sebagai gantinya, Anda perlu menetapkan secara eksplisit IFSuntuk cuplikan apa pun yang perilakunya tergantung pada IFS.

Jika IFSsecara eksplisit diatur ke nilai yang diperlukan (bahkan jika itu kebetulan menjadi nilai default) di setiap baris kode di mana nilai penting menggunakan salah satu dari dua mekanisme yang dijelaskan dalam jawaban ini yang sesuai untuk melokalisasi efek, maka kode keduanya tidak tergantung pada negara global dan tidak akan menghalanginya sama sekali. Pendekatan ini memiliki manfaat tambahan membuatnya sangat eksplisit bagi seseorang yang membaca skrip yang IFSpenting untuk perintah / ekspansi yang satu ini dengan biaya tekstual minimum (dibandingkan dengan bahkan penyimpanan / pengembalian paling dasar).

Lagipula kode apa yang terpengaruh IFS?

Untungnya, tidak ada banyak skenario yang IFSpenting (dengan asumsi Anda selalu mengutip ekspansi Anda ):

  • "$*"dan "${array[*]}"ekspansi
  • doa readbeberapa variabel penargetan bawaan ( read VAR1 VAR2 VAR3) atau variabel array ( read -a ARRAY_VAR_NAME)
  • doa dari readmenargetkan variabel tunggal ketika datang ke terkemuka / trailing spasi atau non-spasi karakter yang muncul di IFS.
  • pemisahan kata (seperti untuk ekspansi tanda kutip, yang mungkin ingin Anda hindari seperti wabah )
  • beberapa skenario lain yang kurang umum (Lihat: IFS @ Greg's Wiki )
sls
sumber
Saya tidak bisa mengatakan saya mengerti Untuk membagi $ PATH bersama: pembatas dengan asumsi tidak ada komponen yang mengandung:: sendiri kalimat. Bagaimana komponen bisa berisi :kapan :pembatas?
Stéphane Chazelas
@ StéphaneChazelas Yah, :adalah karakter yang valid untuk digunakan dalam nama file pada kebanyakan sistem file UNIX / Linux, jadi sangat mungkin untuk memiliki direktori dengan nama yang mengandung :. Mungkin beberapa shell memiliki ketentuan untuk melarikan diri :di PATH dengan menggunakan sesuatu seperti \:, dan kemudian Anda akan melihat kolom muncul yang bukan pembatas yang sebenarnya (Tampaknya bash tidak memungkinkan melarikan diri seperti itu. Fungsi tingkat rendah yang digunakan ketika beralih lebih dari $PATHhanya mencari :di a C string: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls
Saya merevisi jawaban untuk semoga membuat $PATHcontoh pemisahan menjadi :lebih jelas.
sls
1
Selamat datang di SO! Terima kasih atas jawaban mendalamnya :)
Steven Lu
1

Apakah ini praktis? Atau apakah itu pada dasarnya tidak ada gunanya dan saya harus langsung mengatur IFS kembali ke apa pun yang diperlukan untuk penggunaan selanjutnya?

Mengapa beresiko salah ketik pengaturan IFS $' \t\n'ketika semua yang harus Anda lakukan adalah

OIFS=$IFS
do_your_thing
IFS=$OIFS

Atau, Anda dapat memanggil subkulit jika Anda tidak memerlukan variabel apa pun yang diatur / dimodifikasi dalam:

( IFS=:; do_your_thing; )
arielCo
sumber
Ini berbahaya karena tidak berfungsi jika IFSawalnya tidak disetel.
Gilles 'SO- stop being evil'