IFS (Internal Field Separator) ทำหน้าที่เป็นตัวคั่นเดี่ยวสำหรับตัวคั่นหลายตัวต่อเนื่องกันได้หรือไม่?


10

การแยกอาร์เรย์โดยใช้ IFS ด้วยค่าที่ไม่ใช่ขาวสร้างองค์ประกอบที่ว่างเปล่า
แม้แต่การใช้tr -sเพื่อย่อขนาด delim หลาย ๆ ตัวเป็น delim เดียวก็ไม่เพียงพอ
ตัวอย่างอาจอธิบายปัญหาได้ชัดเจนยิ่งขึ้น ..
มีวิธีการที่จะบรรลุผลลัพธ์ "ปกติ" ผ่าน tweaking ของ IFS (มีการตั้งค่าที่เกี่ยวข้องเพื่อเปลี่ยนพฤติกรรมของ 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>"

นี่คือผลลัพธ์


============== 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>

มีดีกว่า (ฉันคิดว่า) ตอบคำถามเดียวกัน : stackoverflow.com/a/14789518/1765658
F. Hauri

คำตอบ:


3

หากต้องการลบหลายตัวอักษรต่อเนื่อง (ไม่ใช่ที่ว่าง) ตัวคั่นพารามิเตอร์สามารถใช้การขยายพารามิเตอร์สองตัว (สตริง / อาร์เรย์) เคล็ดลับคือการตั้งค่าIFSตัวแปรให้เป็นสตริงว่างสำหรับการขยายพารามิเตอร์อาร์เรย์

นี่คือเอกสารman bashภายใต้การแยกคำ :

อาร์กิวเมนต์ null แบบไม่ระบุนัยซึ่งเป็นผลมาจากการขยายตัวของพารามิเตอร์ที่ไม่มีค่าจะถูกลบออก

(
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
)

ดี! วิธีที่ง่ายและมีประสิทธิภาพ - โดยไม่จำเป็นต้องใช้ bash loop และไม่จำเป็นต้องเรียกแอพยูทิลิตี้ - BTW ดังที่คุณพูดถึง"(ไม่ใช่ที่ว่าง)"ฉันจะชี้ให้เห็นเพื่อความชัดเจนว่ามันทำงานได้ดีกับชุดตัวคั่นรวมถึงช่องว่าง
Peter.O

ในการตั้งค่าการทดสอบของฉันIFS=' '(เช่นช่องว่าง) จะทำงานเหมือนกัน ฉันพบนี้ทำให้เกิดความสับสนน้อยกว่าอาร์กิวเมนต์ ( "" หรือ '') null IFSที่ชัดเจนของ
Micha Wiedenmann

นั่นเป็นวิธีการแก้ปัญหาที่แย่มากหากข้อมูลของคุณมีช่องว่างฝังตัว หากข้อมูลของคุณเป็น 'a bc' แทนที่จะเป็น 'abc' IFS = "" จะแบ่ง 'a' เป็นองค์ประกอบแยกจาก 'bc'
Dejay Clayton

5

จากbashmanpage:

อักขระใด ๆ ใน IFS ที่ไม่ใช่ช่องว่าง IFS พร้อมกับอักขระช่องว่าง IFS ที่อยู่ติดกันใด ๆ จะคั่นเขตข้อมูล ลำดับของอักขระช่องว่างของ IFS ยังถือว่าเป็นตัวคั่น

หมายความว่าIFS whitespace (space, tab และ newline) ไม่ได้รับการปฏิบัติเหมือนตัวแยกอื่น หากคุณต้องการที่จะได้รับพฤติกรรมเดียวกันกับตัวคั่นทางเลือกคุณสามารถทำการสลับบางตัวคั่นด้วยความช่วยเหลือของtrหรือ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

%#%#%#%#%สิ่งที่เป็นค่าวิเศษที่จะมาแทนที่ช่องว่างที่เป็นไปได้ภายในเขตที่คาดว่าจะเป็น "ที่ไม่ซ้ำกัน" (หรือมาก unlinkely) หากคุณแน่ใจว่าจะไม่มีที่ว่างในฟิลด์ให้ปล่อยส่วนนี้)


@FussyS ... ขอบคุณ (ดู modificaton ในคำถามของฉัน) ... คุณอาจให้คำตอบกับคำถามที่ตั้งใจ .. และคำตอบนั้นอาจจะ (อาจเป็น) "ไม่มีทางที่จะทำให้ IFS ทำงานได้ ลักษณะที่ฉันต้องการ "... ฉัน trตั้งใจตัวอย่างเพื่อแสดงปัญหา ... ฉันต้องการหลีกเลี่ยงการเรียกระบบดังนั้นฉันจะดูตัวเลือกทุบตีเกินกว่า${var##:}ที่ฉันกล่าวถึงในความคิดเห็นของฉันเพื่อ ansewer ของ Glen .... . ฉันจะรอเวลา .. อาจจะมีวิธีที่จะเล้าโลมไอเอฟเอมิฉะนั้นส่วนแรกของคำตอบของคุณจะเกิดขึ้นหลังจากที่ ....
Peter.O

การรักษาที่IFSจะเหมือนกันในทุกเปลือกหอยบอร์นสไตล์ก็ระบุไว้ใน POSIX
Gilles 'หยุดชั่วร้าย'

4 ปีที่ผ่านมานับตั้งแต่ฉันถามคำถามนี้ฉันพบคำตอบของ @ nazad (โพสต์เมื่อปีที่แล้ว) เพื่อเป็นวิธีที่ง่ายที่สุดในการเล่นปาหี่ IFS เพื่อสร้างอาร์เรย์ที่มีจำนวนและการรวมIFSตัวอักษรเป็นตัวคั่นสตริง คำถามของฉันตอบได้ดีที่สุดjon_dแต่คำตอบของ @nazadแสดงให้เห็นถึงวิธีการใช้งานที่ดีIFSโดยไม่มีลูปและไม่มีแอปยูทิลิตี้
Peter.O

2

เนื่องจาก bash IFS ไม่ได้มีวิธีในการจัดการตัวคั่นต่อเนื่องเป็นตัวคั่นเดียว (สำหรับตัวคั่นที่ไม่ใช่ช่องว่าง) ฉันจึงรวบรวมเวอร์ชันทุบตีทั้งหมดไว้ (เทียบกับการโทรภายนอกเช่น tr, awk, sed )

สามารถจัดการ IFS แบบหลายค่าได้

นี่คือ resu ของเวลาดำเนินการ ts พร้อมกับการทดสอบที่คล้ายกันสำหรับtrและawkตัวเลือกที่แสดงในหน้า Q / A นี้ ... การทดสอบใช้ 10,000 การวนซ้ำของการสร้างอาร์เรย์ (โดยไม่มี I / O) ...

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

นี่คือผลลัพธ์

# 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"

นี่คือสคริปต์

#!/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

เยี่ยมมาก +1 ที่น่าสนใจ!
F. Hauri

1

คุณสามารถทำมันด้วยเพ่งพิศด้วยเช่นกัน แต่ก็ไม่สวย:

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

เอาท์พุท

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"

ขอบคุณ ... ฉันดูเหมือนจะยังไม่ชัดเจนในคำขอหลักของฉัน (คำถามที่แก้ไขแล้ว) ... มันง่ายพอที่จะทำได้โดยเพียงแค่เปลี่ยน$varเป็น${var##:}... ฉันเป็นจริงหลังจากวิธีการปรับแต่ง IFS ตัวเอง .. ฉันต้องการ ทำสิ่งนี้โดยไม่ต้องโทรจากภายนอก (ฉันรู้สึกว่าทุบตีสามารถทำสิ่งนี้ได้อย่างมีประสิทธิภาพมากกว่ากระป๋องภายนอกใด ๆ .. ดังนั้นฉันจะติดตามต่อไป) ... วิธีการของคุณใช้งานได้ (+1) .... เท่าที่ผ่านมา เมื่อปรับเปลี่ยนอินพุตฉันต้องการลองใช้ bash แทน awk หรือ tr (จะหลีกเลี่ยงการเรียกของระบบ) แต่ฉันกำลัง
แฮง

@fred ตามที่กล่าวไว้ IFS slurps ขึ้นหลาย delimeter ต่อเนื่องสำหรับค่าช่องว่างเริ่มต้น มิฉะนั้นตัวคั่นที่ต่อเนื่องกันจะส่งผลให้เกิดช่องว่างที่ไม่เกี่ยวข้อง ฉันคาดหวังว่าการโทรจากภายนอกหนึ่งหรือสองครั้งนั้นไม่น่าจะส่งผลกระทบต่อประสิทธิภาพในทางที่เป็นจริง
เกล็นแจ็คแมน

@glen .. (คุณบอกว่าคำตอบของคุณไม่ได้ "สวย" .. ฉันคิดว่ามันเป็น! :) อย่างไรก็ตามฉันได้รวบรวมเวอร์ชั่นทุบตีทั้งหมด (เทียบกับการโทรจากภายนอก) และตามการวนซ้ำ 10,000 ครั้งของการสร้างอาร์เรย์ ( ไม่ I / O) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... ทำอย่างนั้นสองสามครั้งและคุณอาจคิดว่าการทุบตีช้า! ... ในกรณีนี้ง่ายกว่าไหม? ... ไม่ใช่ถ้าคุณมีตัวอย่างแล้ว :) ... ฉันจะโพสต์ในภายหลัง ต้องไปแล้ว
Peter.O

โดยวิธีการอีกครั้งสคริปต์ gawk ของคุณ ... ฉันไม่เคยใช้ awk ก่อนดังนั้นฉันจึงได้ดูรายละเอียด (และอื่น ๆ ) ... ฉันไม่สามารถเลือกได้ว่าทำไม แต่ฉันจะพูดถึง ปัญหา แต่อย่างใด .. เมื่อได้รับข้อมูลที่ยกมาก็ looses คำพูดและแยกที่ช่องว่างระหว่างคำพูด .. และเกิดปัญหาสำหรับเลขคี่ของคำพูด ... นี่คือข้อมูลการทดสอบ:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O

-1

คำตอบง่ายๆคือ: ยุบตัวคั่นทั้งหมดเป็นหนึ่ง (ตัวแรก)
ที่ต้องมีการวนซ้ำ (ซึ่งทำงานน้อยกว่าlog(N)เวลา):

 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

สิ่งที่เหลืออยู่ที่ต้องทำคือการแยกสตริงอย่างถูกต้องในหนึ่งตัวคั่นและพิมพ์:

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

ไม่จำเป็นต้องset -fเปลี่ยน IFS
ทดสอบกับช่องว่างบรรทัดใหม่และตัวอักษรแบบกลม ทำงานทั้งหมด ค่อนข้างช้า (ควรคาดว่าเป็นวงวนของเชลล์)
แต่สำหรับทุบตีเท่านั้น (ทุบตี 4.4+ เนื่องจากมีตัวเลือก-dในการอ่านใหม่)


ดวลจุดโทษ

เชลล์เวอร์ชันไม่สามารถใช้อาร์เรย์ได้อาร์เรย์เดียวที่มีคือพารามิเตอร์ตำแหน่ง
การใช้tr -sเป็นเพียงหนึ่งบรรทัด (IFS จะไม่เปลี่ยนแปลงในสคริปต์):

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

และพิมพ์:

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

ยังคงช้า แต่ไม่มาก

คำสั่งcommandไม่ถูกต้องใน Bourne
ใน zsh commandเรียกใช้คำสั่งภายนอกเท่านั้นและทำให้ eval ล้มเหลวหากcommandใช้
ใน ksh แม้ว่าจะมีcommandการเปลี่ยนค่าของ IFS ในขอบเขตส่วนกลาง
และcommandทำให้การแบ่งล้มเหลวในเชลล์ที่เกี่ยวข้อง mksh (mksh, lksh, posh) การลบคำสั่งcommandทำให้โค้ดรันบนเชลล์มากขึ้น แต่การลบcommandจะทำให้ IFS รักษามูลค่าไว้ในเชลล์ส่วนใหญ่ (eval เป็น builtin พิเศษ) ยกเว้นใน bash (ไม่มีโหมด posix) และ zsh ในโหมดเริ่มต้น (ไม่มีการจำลอง) แนวคิดนี้ไม่สามารถทำให้การทำงานใน zsh commandเริ่มต้นทั้งที่มีหรือไม่มี


IFS หลายตัว

ใช่ IFS อาจเป็นอักขระหลายตัว แต่อักขระแต่ละตัวจะสร้างอาร์กิวเมนต์หนึ่งตัว:

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

จะส่งออก:

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

ด้วย bash คุณสามารถละเว้นcommandคำนั้นหากไม่อยู่ในการจำลอง sh / POSIX คำสั่งจะล้มเหลวใน ksh93 (IFS เก็บค่าที่เปลี่ยนแปลง) ใน zsh คำสั่งcommandทำให้ zsh พยายามค้นหาevalเป็นคำสั่งภายนอก (ซึ่งไม่พบ) และล้มเหลว

สิ่งที่เกิดขึ้นคือตัวละคร IFS เพียงตัวเดียวที่ถูกยุบอัตโนมัติไปยังตัวคั่นเดียวคือช่องว่างของ IFS
หนึ่งช่องว่างใน IFS จะยุบช่องว่างต่อเนื่องทั้งหมดเป็นหนึ่งช่องว่าง หนึ่งแท็บจะยุบแท็บทั้งหมด หนึ่งช่องว่างและหนึ่งแท็บจะยุบช่องว่างและ / หรือแท็บไปที่หนึ่งตัวคั่น ทำซ้ำความคิดด้วยการขึ้นบรรทัดใหม่

หากต้องการยุบตัวคั่นหลายตัวต้องใช้การเล่นกล
สมมติว่า ASCII 3 (0x03) ไม่ได้ใช้ในอินพุต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.

ความคิดเห็นส่วนใหญ่เกี่ยวกับ ksh, zsh และ bash (เกี่ยวกับcommandและ IFS) ยังคงใช้ที่นี่

ค่าที่$'\0'เป็นไปได้น้อยกว่าในการป้อนข้อความ แต่ตัวแปร bash ไม่สามารถมี NUL ( 0x00)

ไม่มีคำสั่งภายในใน sh ที่จะดำเนินการกับสายอักขระเดียวกันดังนั้น tr เป็นคำตอบเดียวสำหรับสคริปต์ sh


ใช่ฉันเขียนว่าสำหรับเปลือกที่ OP ขอ: Bash ในเชลล์ IFS นั้นจะไม่ถูกเก็บไว้ และใช่ไม่ใช่พกพาไปที่ zsh เช่นกัน @ StéphaneChazelas
Isaac

ในกรณีของ bash และ zsh พวกมันจะทำงานเป็น POSIX ระบุเมื่อเรียกใช้เป็น sh
Stéphane Chazelas

@ StéphaneChazelasเพิ่ม (มาก) หมายเหตุเกี่ยวกับข้อ จำกัด ของแต่ละเชลล์
Isaac

@ StéphaneChazelasทำไมต้องโหวต?
Isaac

ไม่รู้ฉันไม่ใช่ BTW ฉันคิดว่ามีคำถามและคำตอบเฉพาะเกี่ยวกับcommand evalIIRC โดย Gilles
Stéphane Chazelas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.