Dapatkah IFS (Pemisah Bidang Internal) berfungsi sebagai pemisah tunggal untuk beberapa karakter pembatas yang berurutan?

10

Parsing array menggunakan IFS dengan nilai ruang non-putih menciptakan elemen kosong.
Bahkan menggunakan tr -suntuk mengecilkan beberapa batasan ke satu batasan saja tidak cukup.
Sebuah contoh dapat menjelaskan masalah ini dengan lebih jelas ..
Apakah ada cara untuk mencapai hasil "normal" melalui tweaking IFS (apakah ada pengaturan yang terkait untuk mengubah perilaku IFS? .... mis. Untuk bertindak sama seperti ruang kosong standar IFS.

var=" abc  def   ghi    "
echo "============== IFS=<default>"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
#
sfi="$IFS" ; IFS=':'
set -f # Disable file name generation (globbing)
       # (This  data won't "glob", but unless globbing     
       #  is actually needed, turn if off, because   
       #  unusual/unexpected combinations of data can glob!
       #  and they can do it in the most obscure ways...  
       #  With IFS, "you're not in Kansas any more! :)  
var=":abc::def:::ghi::::"
echo "============== IFS=$IFS"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
echo "============== IFS=$IFS and tr"
arr=($(echo -n "$var"|tr -s "$IFS"))
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
set +f     # enable globbing 
IFS="$sfi" # re-instate original IFS val
echo "============== IFS=<default>"

Ini outputnya


============== IFS=<default>
# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
============== IFS=:
# arr[0] ""
# arr[1] "abc"
# arr[2] ""
# arr[3] "def"
# arr[4] ""
# arr[5] ""
# arr[6] "ghi"
# arr[7] ""
# arr[8] ""
# arr[9] ""
============== IFS=: and tr
# arr[0] ""
# arr[1] "abc"
# arr[2] "def"
# arr[3] "ghi"
============== IFS=<default>
Peter.O
sumber
Ada jawaban (saya pikir) yang lebih baik untuk pertanyaan yang sama : stackoverflow.com/a/14789518/1765658
F. Hauri

Jawaban:

3

Untuk menghapus beberapa karakter pembatas berturut-turut (non-spasi), dua (string / array) ekspansi parameter dapat digunakan. Caranya adalah dengan mengatur IFSvariabel ke string kosong untuk ekspansi parameter array.

Ini didokumentasikan di man bashbawah Pemisahan Kata :

Argumen nol implisit yang tidak dikutip, yang dihasilkan dari perluasan parameter yang tidak memiliki nilai, dihapus.

(
set -f
str=':abc::def:::ghi::::'
IFS=':'
arr=(${str})
IFS=""
arr=(${arr[@]})

echo ${!arr[*]}

for ((i=0; i < ${#arr[@]}; i++)); do 
   echo "${i}: '${arr[${i}]}'"
done
)
nazad
sumber
Baik! Metode sederhana dan efektif - tanpa perlu bash loop dan tidak perlu memanggil aplikasi utilitas - BTW. Seperti yang Anda sebutkan "(non-spasi)" , saya akan menunjukkan, untuk kejelasan, bahwa itu berfungsi baik dengan kombinasi karakter pembatas, termasuk ruang.
Peter.O
Dalam pengaturan tes saya IFS=' '(yaitu spasi) berperilaku sama. Saya menemukan ini kurang membingungkan daripada argumen nol eksplisit ("" atau '') dari IFS.
Micha Wiedenmann
Itu semacam solusi yang mengerikan jika data Anda mengandung spasi kosong. Ini, jika data Anda 'a bc' bukan 'abc', IFS = "" akan membagi 'a' menjadi elemen yang terpisah dari 'bc'.
Dejay Clayton
5

Dari bashhalaman manual:

Setiap karakter dalam IFS yang bukan spasi IFS, bersama dengan karakter spasi IFS yang berdekatan, membatasi bidang. Urutan karakter spasi putih IFS juga diperlakukan sebagai pembatas.

Ini berarti bahwa spasi putih IFS (spasi, tab dan baris baru) tidak diperlakukan seperti pemisah lainnya. Jika Anda ingin mendapatkan perilaku yang persis sama dengan pemisah alternatif, Anda dapat melakukan swapping pemisah dengan bantuan tratau sed:

var=":abc::def:::ghi::::"
arr=($(echo -n $var | sed 's/ /%#%#%#%#%/g;s/:/ /g'))
for x in ${!arr[*]} ; do
   el=$(echo -n $arr | sed 's/%#%#%#%#%/ /g')
   echo "# arr[$x] \"$el\""
done

The %#%#%#%#%hal adalah nilai sihir untuk menggantikan ruang mungkin dalam bidang, diharapkan menjadi "unik" (atau sangat unlinkely). Jika Anda yakin tidak akan ada ruang di bidang ini, taruh saja bagian ini).

jon_d
sumber
@FussyS ... Terima kasih (lihat modificaton dalam pertanyaan saya) ... Anda mungkin telah memberi saya jawaban atas pertanyaan yang saya maksudkan .. dan jawaban itu mungkin (mungkin) "Tidak ada cara untuk membuat IFS berperilaku di cara saya inginkan "... Saya bermaksud trcontoh untuk menunjukkan masalah ... Saya ingin menghindari panggilan sistem, jadi saya akan melihat opsi bash di luar ${var##:}yang saya sebutkan dalam komentar saya untuk glen ansewer .... Saya akan menunggu sebentar .. mungkin ada cara untuk membujuk IFS, kalau tidak, bagian pertama dari jawaban Anda adalah setelah ....
Peter.O
Perlakuan IFSitu sama di semua cangkang Bourne-style, itu ditentukan dalam POSIX .
Gilles 'SO- stop being evil'
4-plus tahun sejak saya mengajukan pertanyaan ini - saya menemukan jawaban @ nazad (diposting lebih dari setahun yang lalu) menjadi cara paling sederhana untuk menyulap IFS untuk membuat array dengan nomor dan kombinasi IFSkarakter sebagai pembatas-string. Pertanyaan saya paling baik dijawab jon_d, tetapi jawaban @ nazad menunjukkan cara yang bagus untuk digunakan IFStanpa loop dan tanpa aplikasi utilitas.
Peter.O
2

Karena bash IFS tidak menyediakan cara in-house untuk memperlakukan karakter pembatas berturut-turut sebagai pembatas tunggal (untuk pembatas non-spasi putih), saya telah menyusun versi semua bash (dengan menggunakan panggilan eksternal mis. Tr, awk, sed )

Itu dapat menangani multi-char IFS ..

Berikut ini adalah waktu pelaksanaannya, bersama dengan tes serupa untuk trdan awkopsi yang ditampilkan pada halaman T / A ini ... Tes didasarkan pada 10.000 iterasi dari hanya membangun array (tanpa I / O) ...

pure bash     3.174s (28 char IFS)
call (awk) 0m32.210s  (1 char IFS) 
call (tr)  0m32.178s  (1 char IFS) 

Ini outputnya

# dlm_str  = :.~!@#$%^&()_+-=`}{][ ";></,
# original = :abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'single*quote?'..123:
# unified  = :abc::::def::::::::::::::::::::::::::::'single*quote?'::123:
# max-w 2^ = ::::::::::::::::
# shrunk.. = :abc:def:'single*quote?':123:
# arr[0] "abc"
# arr[1] "def"
# arr[2] "'single*quote?'"
# arr[3] "123"

Ini skripnya

#!/bin/bash

# Note: This script modifies the source string. 
#       so work with a copy, if you need the original. 
# also: Use the name varG (Global) it's required by 'shrink_repeat_chars'
#
# NOTE: * asterisk      in IFS causes a regex(?) issue,     but  *  is ok in data. 
# NOTE: ? Question-mark in IFS causes a regex(?) issue,     but  ?  is ok in data. 
# NOTE: 0..9 digits     in IFS causes empty/wacky elements, but they're ok in data.
# NOTE: ' single quote  in IFS; don't know yet,             but  '  is ok in data.
# 
function shrink_repeat_chars () # A 'tr -s' analog
{
  # Shrink repeating occurrences of char
  #
  # $1: A string of delimiters which when consecutively repeated and are       
  #     considered as a shrinkable group. A example is: "   " whitespace delimiter.
  #
  # $varG  A global var which contains the string to be "shrunk".
  #
# echo "# dlm_str  = $1" 
# echo "# original = $varG" 
  dlms="$1"        # arg delimiter string
  dlm1=${dlms:0:1} # 1st delimiter char  
  dlmw=$dlm1       # work delimiter  
  # More than one delimiter char
  # ============================
  # When a delimiter contains more than one char.. ie (different byte` values),    
  # make all delimiter-chars in string $varG the same as the 1st delimiter char.
  ix=1;xx=${#dlms}; 
  while ((ix<xx)) ; do # Where more than one delim char, make all the same in varG  
    varG="${varG//${dlms:$ix:1}/$dlm1}"
    ix=$((ix+1))
  done
# echo "# unified  = $varG" 
  #
  # Binary shrink
  # =============
  # Find the longest required "power of 2' group needed for a binary shrink
  while [[ "$varG" =~ .*$dlmw$dlmw.* ]] ; do dlmw=$dlmw$dlmw; done # double its length
# echo "# max-w 2^ = $dlmw"
  #
  # Shrik groups of delims to a single char
  while [[ ! "$dlmw" == "$dlm1" ]] ; do
    varG=${varG//${dlmw}$dlm1/$dlm1}
    dlmw=${dlmw:$((${#dlmw}/2))}
  done
  varG=${varG//${dlmw}$dlm1/$dlm1}
# echo "# shrunk.. = $varG"
}

# Main
  varG=':abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'\''single*quote?'\''..123:' 
  sfi="$IFS"; IFS=':.~!@#$%^&()_+-=`}{][ ";></,' # save original IFS and set new multi-char IFS
  set -f                                         # disable globbing
  shrink_repeat_chars "$IFS" # The source string name must be $varG
  arr=(${varG:1})    # Strip leading dlim;  A single trailing dlim is ok (strangely
  for ix in ${!arr[*]} ; do  # Dump the array
     echo "# arr[$ix] \"${arr[ix]}\""
  done
  set +f     # re-enable globbing   
  IFS="$sfi" # re-instate the original IFS
  #
exit
Peter.O
sumber
Kerja bagus, menarik +1!
F. Hauri
1

Anda juga bisa melakukannya dengan gawk, tetapi tidak cantik:

var=":abc::def:::ghi::::"
out=$( gawk -F ':+' '
  {
    # strip delimiters from the ends of the line
    sub("^"FS,"")
    sub(FS"$","")
    # then output in a bash-friendly format
    for (i=1;i<=NF;i++) printf("\"%s\" ", $i)
    print ""
  }
' <<< "$var" )
eval arr=($out)
for x in ${!arr[*]} ; do
  echo "# arr[$x] \"${arr[x]}\""
done

output

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
glenn jackman
sumber
Terima kasih ... Saya tampaknya belum jelas dalam permintaan utama saya (pertanyaan yang dimodifikasi) ... Cukup mudah untuk melakukannya dengan hanya mengubah $varke ${var##:}... Saya benar-benar mencari cara untuk men-tweak IFS itu sendiri .. Saya ingin untuk melakukan ini tanpa panggilan eksternal (Saya punya perasaan bahwa bash dapat melakukan ini dengan lebih efisien daripada yang eksternal .. jadi saya akan tetap menggunakan jalur itu) ... metode Anda berfungsi (+1) .... Sejauh ini saat memodifikasi input, saya lebih suka mencobanya dengan bash, daripada awk atau tr (itu akan menghindari system call), tapi saya benar-benar nongkrong untuk tweak IFS ...
Peter.O
@ Fred, seperti yang disebutkan, IFS hanya menyeruput beberapa delimeter berturut-turut untuk nilai spasi putih default. Jika tidak, pembatas berurutan menghasilkan bidang kosong yang tidak ada. Saya perkirakan satu atau dua panggilan eksternal sangat tidak mungkin memengaruhi kinerja dengan cara apa pun.
glenn jackman
@ Glen .. (Anda mengatakan jawaban Anda bukan "cantik" .. Saya pikir itu! :) Namun, saya telah mengumpulkan semua versi bash (vs panggilan eksternal) dan berdasarkan 10.000 iterasi dari hanya membangun array ( tidak I / O) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Lakukan itu beberapa kali dan Anda mungkin berpikir bash lambat! ... Apakah awk lebih mudah dalam hal ini? ... tidak jika Anda sudah memiliki snippet :) ... Saya akan mempostingnya nanti; harus pergi sekarang.
Peter.O
Ngomong-ngomong, ulangi skrip gawk Anda ... Saya pada dasarnya tidak menggunakan awk sebelumnya, jadi saya telah melihatnya (dan lainnya) secara detail ... Saya tidak bisa memilih alasannya, tapi saya akan menyebutkan masalah bagaimanapun .. Ketika diberikan data yang dikutip, ia kehilangan tanda kutip, dan terbelah di antara tanda kutip .. dan crash untuk jumlah tanda kutip yang aneh ... Inilah data pengujian:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O
-1

Jawaban sederhananya adalah: tutup semua pembatas menjadi satu (yang pertama).
Itu membutuhkan loop (yang berjalan kurang dari log(N)kali):

 var=':a bc::d ef:#$%_+$$%      ^%&*(*&*^
 $#,.::ghi::*::'                           # a long test string.
 d=':@!#$%^&*()_+,.'                       # delimiter set
 f=${d:0:1}                                # first delimiter
 v=${var//["$d"]/"$f"};                    # convert all delimiters to
 :                                         # the first of the delimiter set.
 tmp=$v                                    # temporal variable (v).
 while
     tmp=${tmp//["$f"]["$f"]/"$f"};        # collapse each two delimiters to one
     [[ "$tmp" != "$v" ]];                 # If there was a change
 do
     v=$tmp;                               # actualize the value of the string.
 done

Yang harus dilakukan adalah memisahkan string dengan benar pada satu pembatas, dan mencetaknya:

 readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
 printf '<%s>' "${arr[@]}" ; echo

Tidak perlu set -fatau untuk mengubah IFS.
Diuji dengan spasi, baris baru, dan karakter glob. Semua bekerja Cukup lambat (seperti lingkaran shell seharusnya diharapkan).
Tetapi hanya untuk bash (bash 4.4+ karena opsi -duntuk readarray).


SH

Versi shell tidak dapat menggunakan array, satu-satunya array yang tersedia adalah parameter posisi.
Menggunakan tr -shanya satu baris (IFS tidak berubah dalam skrip):

 set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'

Dan cetak:

 printf '<%s>' "$@" ; echo

Masih lambat, tapi tidak lebih.

Perintah commandtidak valid di Bourne.
Di zsh, commandpanggilan hanya perintah eksternal dan membuat eval gagal jika commanddigunakan.
Di ksh, bahkan dengan command, nilai IFS diubah dalam lingkup global.
Dan commandmembuat pemecahan gagal dalam shell terkait mksh (mksh, lksh, posh) Menghapus perintah commandmembuat kode dijalankan pada lebih banyak shell. Tetapi: menghapus commandakan membuat IFS mempertahankan nilainya di sebagian besar shell (eval adalah builtin khusus) kecuali dalam bash (tanpa mode posix) dan zsh dalam mode default (tanpa emulasi). Konsep ini tidak dapat dibuat berfungsi di zsh default baik dengan atau tanpa command.


Beberapa karakter IFS

Ya, IFS bisa multi karakter, tetapi setiap karakter akan menghasilkan satu argumen:

 set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
 printf '<%s>' "$@" ; echo

Akan menghasilkan:

 <><a bc><><d ef><><><><><><><><><      ><><><><><><><><><
 ><><><><><><ghi><><><><><>

Dengan bash, Anda dapat menghilangkan commandkata jika tidak di emulasi sh / POSIX. Perintah akan gagal di ksh93 (IFS menyimpan nilai yang diubah). Di zsh perintah commandmembuat zsh mencoba mencari evalsebagai perintah eksternal (yang tidak ditemukan) dan gagal.

Apa yang terjadi adalah bahwa satu-satunya karakter IFS yang secara otomatis diciutkan ke satu pembatas adalah ruang putih IFS.
Satu ruang di IFS akan menciutkan semua ruang berurutan menjadi satu. Satu tab akan menciutkan semua tab. Satu spasi dan satu tab akan menciutkan run spasi dan / atau tab menjadi satu pembatas. Ulangi ide dengan baris baru.

Untuk meruntuhkan beberapa pembatas beberapa juggling diperlukan.
Dengan asumsi ASCII 3 (0x03) tidak digunakan dalam input var:

 var=${var// /$'\3'}                       # protect spaces
 var=${var//["$d"]/ }                      # convert all delimiters to spaces
 set -f;                                   # avoid expanding globs.
 IFS=" " command eval set -- '""$var""'    # split on spaces.
 set -- "${@//$'\3'/ }"                    # convert spaces back.

Sebagian besar komentar tentang ksh, zsh dan bash (about commanddan IFS) masih berlaku di sini.

Nilai $'\0'akan kurang mungkin dalam input teks, tetapi variabel bash tidak dapat berisi NUL ( 0x00).

Tidak ada perintah internal di sh untuk melakukan operasi string yang sama, jadi tr adalah satu-satunya solusi untuk skrip sh.

Ishak
sumber
Ya, saya menulis itu untuk shell yang diminta OP: Bash. Dalam shell itu IFS tidak disimpan. Dan ya, tidak portabel, ke zsh, misalnya. @ StéphaneChazelas
Isaac
Dalam kasus bash dan zsh, mereka berperilaku seperti ditentukan POSIX ketika dipanggil sebagai sh
Stéphane Chazelas
@ StéphaneChazelas Menambahkan (banyak) catatan tentang batasan setiap shell.
Isaac
@ StéphaneChazelas Mengapa downvote?
Isaac
Tidak tahu, bukan saya. BTW, saya pikir ada tanya jawab khusus di sini tentang command evalIIRC oleh Gilles
Stéphane Chazelas