Đặt IFS cho một câu lệnh


42

Tôi biết rằng giá trị IFS tùy chỉnh có thể được đặt cho phạm vi của một lệnh / tích hợp sẵn. Có cách nào để đặt giá trị IFS tùy chỉnh cho một câu lệnh không ?? Rõ ràng là không, vì dựa trên giá trị IFS toàn cầu bên dưới bị ảnh hưởng khi điều này được thử

#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

Câu trả lời:


39

Trong một số shell (bao gồm bash):

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

(với bash, bạn có thể bỏ qua phần commandmô phỏng sh / POSIX) nếu không. Nhưng hãy cẩn thận khi sử dụng các biến không được trích dẫn, nhìn chung bạn cũng cần set -fvà không có phạm vi cục bộ cho điều đó trong hầu hết các shell.

Với zsh, bạn có thể làm:

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

$=PATHlà để buộc chia tách từ mà không được thực hiện theo mặc định zsh(việc mở rộng khi mở rộng biến cũng không được thực hiện nên bạn không cần set -ftrừ khi trong mô phỏng sh).

(){...}(hoặc function {...}) được gọi là các hàm ẩn danh và thường được sử dụng để đặt phạm vi cục bộ. với các shell khác hỗ trợ phạm vi cục bộ trong các hàm, bạn có thể làm điều gì đó tương tự với:

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

Để triển khai phạm vi cục bộ cho các biến và tùy chọn trong shell POSIX, bạn cũng có thể sử dụng các hàm được cung cấp tại https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh . Sau đó, bạn có thể sử dụng nó như:

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

(bằng cách này, không hợp lệ để phân chia $PATHtheo cách đó, ngoại trừ zshnhư trong các shell khác, IFS là dấu phân cách trường, không phải là dấu tách trường).

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

Chỉ là hai bài tập, hết lần này đến lần khác a=1 b=2.

Một lưu ý giải thích về var=value cmd:

Trong:

var=value cmd arg

Shell thực hiện /path/to/cmdtrong một quy trình mới và thông qua cmdargtrong argv[]var=valuetrong envp[]. Đó không thực sự là một phép gán biến, nhưng nhiều biến môi trường hơn cho lệnh được thực thi . Trong vỏ Bourne hoặc Korn, với set -k, bạn thậm chí có thể viết nó cmd var=value arg.

Bây giờ, điều đó không áp dụng cho các nội dung hoặc hàm không được thực thi . Trong shell Bourne var=value some-builtin, varcuối cùng , được thiết lập sau đó, giống như với var=valuemột mình. Điều đó có nghĩa là, ví dụ, hành vi của var=value echo foo(không hữu ích) thay đổi tùy thuộc vào việc có echođược dựng sẵn hay không.

POSIX và / hoặc kshthay đổi rằng trong đó hành vi của Bourne chỉ xảy ra đối với một loại nội dung được gọi là nội dung đặc biệt . evallà một nội dung đặc biệt, readkhông. Đối với nội dung không đặc biệt, chỉ var=value builtinthiết lập varcho việc thực thi nội dung khiến nó hoạt động tương tự như khi một lệnh bên ngoài đang được chạy.

Các commandlệnh có thể được sử dụng để loại bỏ các đặc biệt thuộc tính của những builtins đặc biệt . Điều mà POSIX đã bỏ qua là đối với các nội trang eval.nội dung, điều đó có nghĩa là các shell sẽ phải thực hiện một ngăn xếp biến (mặc dù nó không chỉ định các lệnh localhoặc typesetgiới hạn phạm vi), bởi vì bạn có thể làm:

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

Hoặc thậm chí:

a=1 command eval myfunction

với myfunctionchức năng sử dụng hoặc cài đặt $avà có khả năng gọi command eval.

Đó thực sự là một sự bỏ qua bởi vì ksh(mà thông số kỹ thuật chủ yếu dựa trên) đã không thực hiện nó (và AT & T kshzshvẫn không), nhưng ngày nay, ngoại trừ hai, hầu hết các shell đều thực hiện nó. Hành vi khác nhau giữa các vỏ mặc dù trong những thứ như:

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

Tuy nhiên. Sử dụng các localshell hỗ trợ nó là một cách đáng tin cậy hơn để thực hiện phạm vi cục bộ.


Thật kỳ lạ, chỉ IFS=: command eval …đặt IFStrong khoảng thời gian eval, như được ủy quyền bởi POSIX, trong dấu gạch ngang, pdksh và bash, nhưng không phải trong ksh 93u. Thật bất thường khi thấy ksh là người không tuân thủ một cách kỳ quặc.
Gilles 'SO- ngừng trở nên xấu xa'

12

Lưu và khôi phục tiêu chuẩn được lấy từ "Môi trường lập trình Unix" của Kernighan và Pike:

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

2
cảm ơn và +1. Có, tôi biết tùy chọn này, nhưng tôi muốn biết liệu có tùy chọn "sạch" hơn không nếu bạn biết ý tôi là gì
iruvar

Bạn có thể kẹt nó trên một dòng bằng dấu chấm phẩy, nhưng tôi không nghĩ nó sạch hơn. Thật tuyệt nếu mọi thứ bạn muốn diễn đạt đều có sự hỗ trợ cú pháp đặc biệt, nhưng sau đó có lẽ chúng ta phải học nghề mộc hoặc sumptin thay vì viết mã;)
msw

9
Điều đó không thể khôi phục $IFSchính xác nếu nó chưa được đặt trước đó.
Stéphane Chazelas

2
Nếu nó không được đặt, Bash xử lý nó như $'\t\n'' ', như đã giải thích ở đây: wiki.bash-hackers.org/syntax/Exansion/ trộm
davide

2
@davide, đó sẽ là $' \t\n'. không gian phải là đầu tiên như được sử dụng cho "$*". Lưu ý rằng nó giống nhau trong tất cả các vỏ giống như Bourne.
Stéphane Chazelas

8

Đặt tập lệnh của bạn vào một hàm và gọi hàm đó truyền các đối số dòng lệnh cho nó. Vì IFS được định nghĩa cục bộ, các thay đổi đối với nó không ảnh hưởng đến IFS toàn cầu.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"

6

Đối với lệnh này:

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

Có một giải pháp thay thế: cung cấp lệnh đầu tiên ( IFS=$'\n') cho lệnh thực thi (một hàm):

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

Điều đó sẽ đặt IFS trong môi trường để gọi phân tách, nhưng sẽ không được giữ lại trong môi trường hiện tại.

Điều này cũng tránh việc sử dụng eval luôn có rủi ro.


Trong ksh93 và mksh, và bash và zsh khi ở chế độ POSIX, điều đó vẫn $IFSđược đặt thành $'\n'sau đó theo yêu cầu của POSIX.
Stéphane Chazelas

4

Câu trả lời được đề xuất từ ​​@helpermethod chắc chắn là một cách tiếp cận thú vị. Nhưng đó cũng là một cái bẫy vì trong phạm vi biến cục bộ BASH kéo dài từ người gọi đến hàm được gọi. Do đó, đặt IFS trong hàm main (), sẽ dẫn đến giá trị đó vẫn tồn tại cho các hàm được gọi từ hàm main (). Đây là một ví dụ:

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

Và đầu ra ...

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

Nếu IFS được khai báo trong main () không nằm trong phạm vi trong func (), thì mảng sẽ không được phân tích cú pháp chính xác trong func () B. Bỏ ghi chú dòng đầu tiên trong func () và bạn nhận được kết quả đầu ra này:

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

Đó là những gì bạn sẽ nhận được nếu IFS đã đi ra khỏi phạm vi.

Một giải pháp tốt hơn nhiều IMHO, là từ bỏ thay đổi hoặc dựa vào IFS ở cấp độ toàn cầu / địa phương. Thay vào đó, sinh ra một lớp vỏ mới và mân mê IFS trong đó. Chẳng hạn, nếu bạn gọi func () trong hàm main () như sau, chuyển mảng dưới dạng một chuỗi bằng dấu phân cách trường gạch chéo ngược:

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

... thay đổi thành IFS sẽ không được phản ánh trong func (). Mảng sẽ được truyền dưới dạng một chuỗi:

ick\blick\flick

... nhưng bên trong func () IFS vẫn sẽ là "/" (như được đặt trong main ()) trừ khi được thay đổi cục bộ trong func ().

Thông tin thêm về cách ly các thay đổi đối với IFS có thể được xem tại các liên kết sau:

Làm cách nào để chuyển đổi một biến mảng bash thành một chuỗi được phân tách bằng các dòng mới?

Chuỗi Bash thành mảng với IFS

Gợi ý và Mẹo để lập trình tập lệnh shell chung - Xem "LƯU Ý việc sử dụng shell phụ ..."


thực sự thú vị ...
iruvar

"Bash chuỗi thành mảng với IFS" IFS=$'\n' declare -a astr=(...)hoàn hảo cảm ơn!
Sức mạnh Bảo Bình

1

Đoạn trích này từ câu hỏi:

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

được đặt xen kẽ là hai phép gán biến toàn cục riêng biệt được đánh giá từ trái sang phải và tương đương với:

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

hoặc là

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

Điều này giải thích cả lý do tại sao toàn cầu IFSđã được sửa đổi và tại sao việc chia từ $strthành các phần tử mảng được thực hiện bằng cách sử dụng giá trị mới của IFS.

Bạn có thể bị cám dỗ sử dụng một lớp con để hạn chế ảnh hưởng của IFSsửa đổi như thế này:

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

nhưng bạn sẽ nhanh chóng nhận thấy rằng việc sửa đổi acũng bị giới hạn ở phần con:

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

Tiếp theo, bạn sẽ được lưu / khôi phục IFS bằng cách sử dụng giải pháp từ câu trả lời trước đó của @msw hoặc thử và sử dụng local IFSmột chức năng bên trong một hàm như được đề xuất bởi @rcpermethod. Nhưng khá sớm, bạn nhận thấy bạn đang gặp nhiều rắc rối, đặc biệt nếu bạn là một tác giả thư viện, người cần phải mạnh mẽ để chống lại các kịch bản gọi sai:

  • Điều gì nếu IFSban đầu không được đặt?
  • Điều gì nếu chúng ta đang chạy với set -u(aka set -o nounset)?
  • Điều gì nếu IFSđược thực hiện chỉ đọc qua declare -r IFS?
  • Điều gì xảy ra nếu tôi cần cơ chế lưu / khôi phục để hoạt động với đệ quy và hoặc thực thi không đồng bộ (chẳng hạn như trình trapxử lý`)?

Vui lòng không lưu / khôi phục IFS. Thay vào đó, hãy kiên trì sửa đổi tạm thời:

  • Để giới hạn sửa đổi biến thành một lệnh, hàm dựng sẵn hoặc gọi hàm, hãy sử dụng IFS="value" command.

    • Để đọc thành nhiều biến bằng cách tách trên một ký tự cụ thể ( :được sử dụng dưới đây làm ví dụ), hãy sử dụng:

      IFS=":" read -r var1 var2 <<< "$str"
    • Để đọc vào một mảng sử dụng (làm điều này thay vì array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • Hạn chế ảnh hưởng của việc sửa đổi biến thành một lớp con.

    • Để xuất các phần tử của mảng được phân tách bằng dấu phẩy:

      (IFS=","; echo "${array[*]}")
    • Để nắm bắt điều đó thành một chuỗi:

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

0

Giải pháp thẳng nhất là lấy một bản sao của bản gốc $IFS, ví dụ như câu trả lời của msw. Tuy nhiên, giải pháp này không phân biệt giữa một unset IFSvà một IFStập hợp bằng chuỗi rỗng, điều này rất quan trọng đối với nhiều ứng dụng. Đây là một giải pháp tổng quát hơn để nắm bắt sự khác biệt này:

# 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.