Pengaturan IFS untuk satu pernyataan


42

Saya tahu bahwa nilai IFS khusus dapat diatur untuk lingkup perintah tunggal / built-in. Apakah ada cara untuk menetapkan nilai IFS khusus untuk satu pernyataan? Tampaknya tidak, karena berdasarkan di bawah ini nilai IFS global terpengaruh ketika ini dicoba

#check environment IFS value, it is space-tab-newline
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003
#invoke built-in with custom IFS
IFS=$'\n' read -r -d '' -a arr <<< "$str"
#environment IFS value remains unchanged as seen below
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003

#now attempt to set IFS for a single statement
IFS=$'\n' a=($str)
#BUT environment IFS value is overwritten as seen below
printf "%s" "$IFS" | od -bc
0000000 012
         \n
     0000001

Jawaban:


39

Dalam beberapa shell (termasuk bash):

IFS=: command eval 'p=($PATH)'

(dengan bash, Anda dapat menghilangkan commandemulasi jika tidak dalam sh / POSIX). Namun berhati-hatilah bahwa ketika menggunakan variabel yang tidak dikutip, Anda juga umumnya perlu set -f, dan tidak ada cakupan lokal untuk itu di sebagian besar shell.

Dengan zsh, Anda dapat melakukan:

(){ local IFS=:; p=($=PATH); }

$=PATHadalah untuk memaksa pemisahan kata yang tidak dilakukan secara default di zsh(globbing pada ekspansi variabel tidak dilakukan sehingga Anda tidak perlu set -fkecuali dalam emulasi sh).

(){...}(atau function {...}) disebut fungsi anonim dan biasanya digunakan untuk mengatur cakupan lokal. dengan shell lain yang mendukung lingkup fungsi lokal, Anda bisa melakukan sesuatu yang mirip dengan:

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'

Untuk menerapkan lingkup lokal untuk variabel dan opsi dalam cangkang POSIX, Anda juga dapat menggunakan fungsi yang disediakan di https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh . Maka Anda dapat menggunakannya sebagai:

. /path/to/locvar.sh
var=3,2,2
call eval 'locvar IFS; locopt -f; IFS=,; set -- $var; a=$1 b=$2 c=$3'

(Ngomong-ngomong, tidak sah untuk memecah $PATHseperti itu di atas kecuali dalam zshseperti pada shell lainnya, IFS adalah pembatas lapangan, bukan pemisah bidang).

IFS=$'\n' a=($str)

Apakah hanya dua tugas, satu demi satu sama suka a=1 b=2.

Catatan penjelasan tentang var=value cmd:

Di:

var=value cmd arg

Shell dieksekusi /path/to/cmddalam proses baru dan melewati cmddan argmasuk argv[]dan var=valuemasuk envp[]. Itu bukan benar-benar tugas variabel, tetapi lebih banyak mengirimkan variabel lingkungan ke perintah yang dieksekusi . Dalam shell Bourne atau Korn, dengan set -k, Anda bahkan dapat menulisnya cmd var=value arg.

Sekarang, itu tidak berlaku untuk builtin atau fungsi yang tidak dijalankan . Dalam cangkang Bourne, dalam var=value some-builtin, varakhirnya ditetapkan setelahnya, sama seperti dengan var=valuesendirian. Itu berarti misalnya bahwa perilaku var=value echo foo(yang tidak berguna) bervariasi tergantung pada apakah echobuiltin atau tidak.

POSIX dan / atau kshmengubahnya dalam perilaku Bourne yang hanya terjadi untuk kategori builtin yang disebut builtin khusus . evaladalah builtin khusus, readbukan. Untuk builtin non khusus, var=value builtinset varhanya untuk eksekusi builtin yang membuatnya berperilaku mirip dengan ketika perintah eksternal dijalankan.

The commandperintah dapat digunakan untuk menghapus khusus atribut yang builtin khusus . Apa POSIX diabaikan adalah bahwa untuk evaldan .builtin, itu berarti bahwa shell harus mengimplementasikan tumpukan variabel (meskipun itu tidak menentukan perintah localatau typesetruang lingkup membatasi), karena Anda bisa melakukan:

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a

Atau bahkan:

a=1 command eval myfunction

dengan myfunctionmenjadi fungsi menggunakan atau pengaturan $adan berpotensi memanggil command eval.

Itu benar-benar diabaikan karena ksh(yang sebagian besar didasarkan pada spesifikasi) tidak mengimplementasikannya (dan AT&T kshdan zshmasih tidak), tetapi saat ini, kecuali kedua, sebagian besar shell mengimplementasikannya. Perilaku bervariasi di antara cangkang meskipun dalam hal-hal seperti:

a=0; a=1 command eval a=2; echo "$a"

meskipun. Menggunakan localshell yang mendukungnya adalah cara yang lebih dapat diandalkan untuk mengimplementasikan cakupan lokal.


Anehnya, hanya IFS=: command eval …menetapkan IFSuntuk durasi eval, seperti yang diamanatkan oleh POSIX, di dash, pdksh dan bash, tetapi tidak di ksh 93u. Tidak biasa melihat ksh menjadi orang aneh-tidak-patuh.
Gilles 'SANGAT berhenti menjadi jahat'

12

Hemat-dan-pengembalian standar diambil dari "Lingkungan Pemrograman Unix" oleh Kernighan dan Pike:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}

2
terima kasih dan +1. Ya saya mengetahui opsi ini, tetapi saya ingin tahu apakah ada opsi "bersih" jika Anda tahu maksud saya
iruvar

Anda bisa memasukkannya ke dalam satu baris dengan titik koma, tapi saya rasa itu tidak bersih. Mungkin menyenangkan jika segala sesuatu yang ingin Anda ungkapkan memiliki dukungan sintaksis khusus, tetapi kemudian kita mungkin harus belajar pertukangan atau sumptin alih-alih mengodekan;)
msw

9
Itu gagal mengembalikan $IFSdengan benar jika sebelumnya tidak disetel.
Stéphane Chazelas

2
Jika tidak disetel, Bash memperlakukannya seperti $'\t\n'' ', sebagaimana dijelaskan di sini: wiki.bash-hackers.org/syntax/expansion/…
davide

2
@davide, itu akan menjadi $' \t\n'. ruang harus menjadi yang pertama seperti yang digunakan untuk "$*". Perhatikan bahwa itu sama di semua kerang mirip Bourne.
Stéphane Chazelas

8

Masukkan skrip Anda ke dalam suatu fungsi dan aktifkan fungsi yang meneruskan argumen commandline ke dalamnya. Karena IFS didefinisikan lokal, perubahan itu tidak mempengaruhi IFS global.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"

6

Untuk perintah ini:

IFS=$'\n' a=($str)

Ada solusi alternatif: untuk memberikan tugas pertama ( IFS=$'\n') perintah untuk dieksekusi (fungsi):

$ split(){ a=( $str ); }
$ IFS=$'\n' split

Itu akan menempatkan IFS di lingkungan untuk memanggil split, tetapi tidak akan dipertahankan di lingkungan saat ini.

Ini juga menghindari penggunaan eval yang selalu berisiko.


Dalam ksh93 dan mksh, dan bash dan zsh ketika dalam mode POSIX, yang masih menyisakan $IFSdiatur $'\n'setelah itu seperti yang disyaratkan oleh POSIX.
Stéphane Chazelas

4

Jawaban yang diajukan dari @helpermethod tentu saja merupakan pendekatan yang menarik. Tapi itu juga sedikit jebakan karena dalam BASH lingkup variabel lokal meluas dari pemanggil ke fungsi yang dipanggil. Oleh karena itu, pengaturan IFS di main (), akan menghasilkan nilai yang bertahan untuk fungsi yang dipanggil dari main (). Ini sebuah contoh:

#!/usr/bin/env bash
#
func() {
  # local IFS='\'

  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local f_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#f_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${f_args[$i]}  "
  done
  echo
}

main() {
  local IFS='/'

  # the rest goes here
  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local m_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#m_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${m_args[$i]}  "
  done
  echo

  func "${m_args[*]}"
}

main "$@"

Dan hasilnya ...

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick  [1]: blick  [2]: flick

Jika IFS dideklarasikan di main () tidak masih dalam lingkup di func (), maka array tidak akan diuraikan dengan benar di func () B. Batalkan komentar pada baris pertama di func () dan Anda mendapatkan output ini:

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick/blick/flick

Yang harus Anda dapatkan jika IFS keluar dari ruang lingkup.

Solusi IMHO yang jauh lebih baik, adalah melepaskan perubahan atau mengandalkan IFS di tingkat global / lokal. Alih-alih, buat shell baru dan bermain-main dengan IFS di sana. Misalnya, jika Anda memanggil func () di main () sebagai berikut, meneruskan array sebagai string dengan pemisah bidang garis miring:

func $(IFS='\'; echo "${m_args[*]}")

... bahwa perubahan ke IFS tidak akan tercermin dalam func (). Array akan diteruskan sebagai string:

ick\blick\flick

... tetapi di dalam func () IFS masih akan menjadi "/" (sebagaimana diatur di main ()) kecuali diubah secara lokal di func ().

Informasi lebih lanjut tentang mengisolasi perubahan pada IFS dapat dilihat di tautan berikut:

Bagaimana cara mengonversi variabel array bash ke string yang dibatasi dengan baris baru?

Bash string to array dengan IFS

Petunjuk dan Tip untuk pemrograman skrip shell umum - Lihat "Perhatikan penggunaan sub-shell ..."


menarik memang ...
iruvar

"Bash string to array with IFS", IFS=$'\n' declare -a astr=(...)terima kasih sempurna!
Aquarius Power

1

Cuplikan ini dari pertanyaan:

IFS=$'\n' a=($str)

diinterpretasikan sebagai dua penugasan variabel global terpisah yang dievaluasi dari kiri ke kanan, dan setara dengan:

IFS=$'\n'; a=($str)

atau

IFS=$'\n'
a=($str)

Ini menjelaskan mengapa global IFSdiubah, dan mengapa pemisahan kata $strmenjadi elemen array dilakukan menggunakan nilai baru IFS.

Anda mungkin tergoda untuk menggunakan subkulit untuk membatasi efek IFSmodifikasi seperti ini:

str="value 0:value 1"
a=( old values )
( # Following code runs in a subshell
 IFS=":"
 a=($str)
 printf 'Subshell IFS: %q\n' "${IFS}"
 echo "Subshell: a[0]='${a[0]}' a[1]='${a[1]}'"
)
printf 'Parent IFS: %q\n' "${IFS}"
echo "Parent: a[0]='${a[0]}' a[1]='${a[1]}'"

tetapi Anda akan segera melihat bahwa modifikasi ajuga terbatas pada subkulit:

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'

Selanjutnya, Anda akan tergoda untuk menyimpan / mengembalikan IFS menggunakan solusi dari jawaban sebelumnya oleh @msw atau mencoba dan menggunakan local IFSfungsi di dalam seperti yang disarankan oleh @helpermethod. Tapi segera, Anda melihat Anda berada dalam segala macam masalah, terutama jika Anda adalah seorang penulis perpustakaan yang harus kuat terhadap perilaku yang salah dalam menjalankan skrip:

  • Bagaimana jika IFSawalnya tidak disetel?
  • Bagaimana jika kita menjalankan set -u(alias set -o nounset)?
  • Bagaimana jika IFSdibuat hanya-baca melalui declare -r IFS?
  • Bagaimana jika saya perlu save / mekanisme untuk bekerja memulihkan dengan rekursi dan atau eksekusi asynchronous (seperti traphandler`)?

Tolong jangan simpan / pulihkan IFS. Sebaliknya, tetap gunakan modifikasi sementara:

  • Untuk membatasi modifikasi variabel menjadi satu perintah, pemanggilan fungsi atau built-in, gunakan IFS="value" command.

    • Untuk membaca ke beberapa variabel dengan memisahkan karakter tertentu ( :digunakan sebagai contoh di bawah), gunakan:

      IFS=":" read -r var1 var2 <<< "$str"
    • Untuk membaca penggunaan array (lakukan ini alih-alih array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • Batasi efek dari memodifikasi variabel ke subkulit.

    • Untuk menampilkan elemen array yang dipisahkan oleh koma:

      (IFS=","; echo "${array[*]}")
    • Untuk menangkapnya menjadi string:

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

0

Solusi yang paling lurus ke depan adalah mengambil salinan aslinya $IFS, seperti pada misalnya jawaban msw. Namun, solusi ini tidak membedakan antara set IFSdan IFSset yang tidak sama dengan string kosong, yang penting untuk banyak aplikasi. Berikut adalah solusi yang lebih umum yang menangkap perbedaan ini:

# Functions taking care of IFS
set_IFS(){
    if [ -z "${IFS+x}" ]; then
        IFS_ori="__unset__"
    else
        IFS_ori="$IFS"
    fi
    IFS="$1"
}
reset_IFS(){
    if [ "${IFS_ori}" == "__unset__" ]; then
        unset IFS
    else
        IFS="${IFS_ori}"
    fi
}

# Example of use
set_IFS "something_new"
some_program_or_builtin
reset_IFS
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.