การตั้งค่า IFS สำหรับคำสั่งเดียว


42

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

#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

คำตอบ:


39

ในบางเชลล์ (รวมถึงbash):

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

(ด้วยbash, คุณสามารถละเว้นcommandถ้าไม่อยู่ในการจำลอง sh / POSIX) แต่ระวังว่าเมื่อใช้ตัวแปรที่ไม่ได้กล่าวถึงคุณจำเป็นต้องใช้โดยทั่วไปset -fและไม่มีขอบเขตภายในสำหรับเชลล์ส่วนใหญ่

ด้วย zsh คุณสามารถทำได้:

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

$=PATHคือการบังคับให้แยกคำที่ไม่ได้ทำไว้เป็นค่าเริ่มต้นในzsh(การวนตามการขยายตัวของตัวแปรไม่ได้ทำเช่นนั้นดังนั้นคุณไม่ต้องการset -fยกเว้นในการจำลอง sh)

(){...}(หรือfunction {...}) เรียกว่าฟังก์ชั่นที่ไม่ระบุชื่อและโดยทั่วไปจะใช้ในการตั้งค่าขอบเขตท้องถิ่น ด้วยเชลล์อื่น ๆ ที่สนับสนุนขอบเขตโลคัลในฟังก์ชันคุณสามารถทำสิ่งที่คล้ายกับ:

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

ที่จะใช้ขอบเขตท้องถิ่นสำหรับตัวแปรและตัวเลือกในเปลือกหอย POSIX คุณยังสามารถใช้ฟังก์ชั่นที่มีให้ ณhttps://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh จากนั้นคุณสามารถใช้มันเป็น:

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

(โดยวิธีการที่ไม่สามารถแยก$PATHวิธีดังกล่าวข้างต้นยกเว้นในzshเช่นเดียวกับเชลล์อื่น ๆ IFS เป็นตัวคั่นฟิลด์ไม่ใช่ตัวคั่นฟิลด์)

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

เป็นเพียงการมอบหมายสองอย่างและอีกอันหนึ่งเหมือนa=1 b=2กัน

หมายเหตุของคำอธิบายเกี่ยวกับvar=value cmd:

ใน:

var=value cmd arg

รันเปลือก/path/to/cmdในกระบวนการใหม่และบัตรcmdและargในargv[]และในvar=value envp[]นั่นไม่ใช่การกำหนดตัวแปร แต่เป็นการส่งผ่านตัวแปรสภาพแวดล้อมไปยังคำสั่งที่ดำเนินการ ในเชลล์ Bourne หรือ Korn ด้วยset -kคุณยังสามารถเขียนมันcmd var=value argได้

ตอนนี้ที่ใช้ไม่ได้กับ builtins หรือฟังก์ชั่นที่ไม่ได้ดำเนินการ ใน Bourne shell, in var=value some-builtin, varจบลงด้วยการตั้งค่าในภายหลังเช่นvar=valueเดียวกับคนเดียว นั่นหมายถึงตัวอย่างที่พฤติกรรมของvar=value echo foo(ซึ่งไม่มีประโยชน์) แตกต่างกันไปขึ้นอยู่กับว่าechoมีการสร้างขึ้นหรือไม่

POSIX และ / หรือkshการเปลี่ยนแปลงที่ในการที่ว่าพฤติกรรมของบอร์นจะเกิดขึ้นสำหรับหมวดหมู่ของ builtins ที่เรียกว่าbuiltins พิเศษ evalเป็น builtin พิเศษreadไม่ได้ สำหรับ builtin ที่ไม่ใช่แบบพิเศษให้var=value builtinตั้งค่าvarเฉพาะสำหรับการเรียกใช้งานของ builtin ซึ่งทำให้มันทำงานคล้ายกับเมื่อมีการเรียกใช้คำสั่งภายนอก

commandคำสั่งที่สามารถใช้ในการลบพิเศษแอตทริบิวต์ของผู้ที่builtins พิเศษ สิ่งที่ POSIX มองข้ามคือสำหรับevalและ.บิวด์อินนั่นหมายความว่าเชลล์จะต้องใช้ตัวแปรสแต็ก (แม้ว่าจะไม่ได้ระบุlocalหรือtypesetคำสั่งการ จำกัด ขอบเขต) เนื่องจากคุณสามารถทำได้:

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

หรือแม้กระทั่ง:

a=1 command eval myfunction

ด้วยmyfunctionการเป็นฟังก์ชั่นการใช้หรือการตั้งค่าและอาจเรียก$acommand eval

นั่นเป็นสิ่งที่มองข้ามเพราะksh(ซึ่งข้อมูลจำเพาะส่วนใหญ่อ้างอิงจาก) ไม่ได้ใช้มัน (และ AT&T kshและzshยังไม่ทำ) แต่ทุกวันนี้ยกเว้นทั้งสองเชลล์ส่วนใหญ่ใช้มัน พฤติกรรมแตกต่างกันระหว่างหอยในสิ่งที่ชอบ:

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

แม้ การใช้localบนเชลล์ที่สนับสนุนเป็นวิธีที่เชื่อถือได้มากขึ้นในการใช้ขอบเขตโลคัล


แปลกIFS=: command eval …กำหนดIFSเฉพาะในช่วงเวลาของevalตามที่ได้รับคำสั่งจาก POSIX ในประ, pdksh และทุบตี แต่ไม่ได้อยู่ใน ksh 93u มันผิดปกติที่จะเห็นว่า ksh นั้นเป็นสิ่งที่แปลกและไม่เข้ากันเลย
Gilles 'หยุดความชั่วร้าย'

12

บันทึกและกู้คืนมาตรฐานจาก "The Unix Programming Environment" โดย Kernighan และ Pike:

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

2
ขอบคุณและ +1 ใช่ฉันรู้ตัวเลือกนี้ แต่ผมอยากจะรู้ว่าถ้ามีการ "ทำความสะอาดตัวเลือก" ถ้าคุณรู้ว่าผมหมายถึง
Iruvar

คุณสามารถอัดมันลงในหนึ่งบรรทัดด้วยเซมิโคลอน แต่ฉันไม่คิดว่ามันจะสะอาดกว่านี้ มันอาจจะดีถ้าทุกสิ่งที่คุณอยากจะแสดงได้รับการสนับสนุนประโยคพิเศษ แต่แล้วเราอาจจะต้องเรียนรู้ช่างไม้หรือ sumptin แทนการเข้ารหัส;)
ขยะ

9
ที่ล้มเหลวในการเรียกคืน$IFSอย่างถูกต้องหากก่อนหน้านี้มันไม่มีการตั้งค่า
Stéphane Chazelas

2
หากไม่ได้กำหนดไว้ Bash จะถือว่าเป็น$'\t\n'' 'ตามที่อธิบายไว้ที่นี่: wiki.bash-hackers.org/syntax/expansion/…
davide

2
@davide $' \t\n'ที่จะเป็น "$*"พื้นที่จะต้องมีครั้งแรกในฐานะที่ใช้สำหรับ โปรดทราบว่ามันเหมือนกันในทุก ๆ เชลล์ที่เหมือน Bourne
Stéphane Chazelas

8

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

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"

6

สำหรับคำสั่งนี้:

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

มีทางเลือกอื่น: เพื่อให้การมอบหมายครั้งแรก ( IFS=$'\n') คำสั่งเพื่อดำเนินการ (ฟังก์ชั่น):

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

นั่นจะทำให้ IFS ในสภาพแวดล้อมที่จะเรียกแยก แต่จะไม่ถูกเก็บไว้ในสภาพแวดล้อมปัจจุบัน

นอกจากนี้ยังหลีกเลี่ยงการใช้ eval ที่มีความเสี่ยงอยู่เสมอ


ใน ksh93 และ mksh และ bash และ zsh เมื่ออยู่ในโหมด POSIX ซึ่งยังคงปล่อย$IFSให้ตั้งค่าไว้$'\n'หลังจากนั้นตามที่ POSIX ต้องการ
Stéphane Chazelas

4

คำตอบที่เสนอจาก @helpermethod เป็นวิธีที่น่าสนใจอย่างแน่นอน แต่มันก็เป็นกับดักนิดหน่อยเพราะว่าในขอบเขตของตัวแปรโลคอล BASH นั้นขยายจากตัวเรียกไปยังฟังก์ชันที่เรียก ดังนั้นการตั้งค่า IFS ใน main () จะส่งผลให้ค่านั้นยังคงอยู่กับฟังก์ชั่นที่เรียกว่าจาก main () นี่คือตัวอย่าง:

#!/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 "$@"

และผลผลิต ...

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

หาก IFS ที่ประกาศใน main () ยังไม่อยู่ในขอบเขตใน func () ดังนั้นอาร์เรย์จะไม่ถูกวิเคราะห์อย่างถูกต้องใน func () B. ยกเลิกการใส่เครื่องหมายบรรทัดแรกใน func () และคุณได้ผลลัพธ์นี้:

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

นี่คือสิ่งที่คุณควรได้รับหาก IFS พ้นขอบเขต

ทางออกที่ดีกว่า IMHO คือสละการเปลี่ยนแปลงหรือการพึ่งพา IFS ในระดับโลก / ท้องถิ่น แต่ให้วางไข่ใหม่และเล่นซอกับ IFS แทน ตัวอย่างเช่นหากคุณต้องเรียก func () ใน main () ดังต่อไปนี้ให้ส่งอาร์เรย์เป็นสตริงที่มีตัวคั่นฟิลด์สแลชย้อนหลัง:

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

... การเปลี่ยนเป็น IFS นั้นจะไม่ปรากฏภายใน func () อาร์เรย์จะถูกส่งเป็นสตริง:

ick\blick\flick

... แต่ข้างใน func () IFS จะยังคงเป็น "/" (ตามที่กำหนดไว้ใน main ()) ยกเว้นว่ามีการเปลี่ยนแปลงแบบโลคอลใน func ()

ข้อมูลเพิ่มเติมเกี่ยวกับการแยกการเปลี่ยนแปลงของ IFS สามารถดูได้ที่ลิงค์ต่อไปนี้:

ฉันจะแปลงตัวแปร bash array เป็นสตริงที่คั่นด้วย newlines ได้อย่างไร

Bash string เป็นอาร์เรย์ด้วย IFS

คำแนะนำและเคล็ดลับสำหรับการวางโปรแกรมเชลล์สคริปต์ทั่วไป - ดู "หมายเหตุการใช้เชลล์ย่อย ... "


น่าสนใจแน่นอน ...
iruvar

"Bash string ไปยังอาร์เรย์ด้วย IFS" IFS=$'\n' declare -a astr=(...)ขอบคุณอย่างสมบูรณ์แบบ!
กุมภ์อำนาจ

1

ตัวอย่างนี้จากคำถาม:

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

ถูก interepreted เป็นสองการมอบหมายตัวแปรโกลบอลที่แยกจากกันประเมินจากซ้ายไปขวาและเทียบเท่ากับ:

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

หรือ

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

นี้จะอธิบายว่าทำไมทั้งโลกIFSมีการปรับเปลี่ยนและทำไมคำแยกของเป็นองค์ประกอบอาร์เรย์ถูกดำเนินการโดยใช้ค่าใหม่ของ$strIFS

คุณอาจถูกล่อลวงให้ใช้ subshell เพื่อ จำกัด ผลกระทบของการIFSดัดแปลงเช่นนี้:

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]}'"

แต่คุณจะสังเกตเห็นได้อย่างรวดเร็วว่าการดัดแปลงของaนั้น จำกัด อยู่ที่ subshell ด้วย:

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

ถัดไปคุณจะถูกล่อลวงให้บันทึก / กู้คืน IFS โดยใช้โซลูชันจากคำตอบก่อนหน้านี้โดย @msw หรือลองและใช้local IFSฟังก์ชันภายในตามที่แนะนำโดย @helpermethod แต่ในไม่ช้าคุณจะสังเกตเห็นว่าคุณมีปัญหาทุกประเภทโดยเฉพาะอย่างยิ่งถ้าคุณเป็นผู้แต่งห้องสมุดที่ต้องเข้มแข็งเพื่อต่อต้านสคริปต์การเรียกใช้ที่ไม่เหมาะสม:

  • เกิดอะไรขึ้นถ้าไม่IFSได้ตั้งค่าเริ่มต้น?
  • เกิดอะไรขึ้นถ้าเรากำลังทำงานกับset -u(aka set -o nounset)?
  • เกิดอะไรขึ้นถ้าIFSถูกสร้างขึ้นมาอ่านอย่างเดียวผ่านdeclare -r IFS?
  • ถ้าฉันต้องการกลไกการบันทึก / เรียกคืนเพื่อทำงานกับการเรียกซ้ำและหรือการดำเนินการแบบอะซิงโครนัส (เช่นตัวtrapจัดการ `)

โปรดอย่าบันทึก / กู้คืน IFS ให้ทำตามการแก้ไขชั่วคราวแทน:

  • เพื่อ จำกัด การปรับเปลี่ยนตัวแปรคำสั่งเดียวในตัวหรือฟังก์ชั่นการภาวนา, IFS="value" commandการใช้งาน

    • หากต้องการอ่านหลายตัวแปรโดยการแยกอักขระเฉพาะ ( :ใช้ตัวอย่างด้านล่าง) ให้ใช้:

      IFS=":" read -r var1 var2 <<< "$str"
    • วิธีอ่านการใช้อาเรย์ (ทำสิ่งนี้แทนarray_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • จำกัด ผลกระทบของการแก้ไขตัวแปรไปยังเชลล์ย่อย

    • ในการส่งออกองค์ประกอบของอาร์เรย์คั่นด้วยเครื่องหมายจุลภาค:

      (IFS=","; echo "${array[*]}")
    • ในการจับภาพนั้นเป็นสตริง:

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

0

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

# 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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.