Встановлення 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[]. Це насправді не присвоєння змінної, але більше змінних середовищ змінних виконуваної команди. У оболонці Борна чи Корна, з set -k, можна навіть записати cmd var=value arg.

Тепер це не стосується вбудованих або функцій, які не виконуються . В Bourne оболонки, в var=value some-builtin, в varкінці кінців бути встановлений після цього, так само , як з в var=valueпоодинці. Це означає, наприклад, що поведінка var=value echo foo(що не корисно) змінюється залежно від того echo, будується вона чи ні.

POSIX та / або kshзмінили це в тому, що поведінка Борна відбувається лише для категорії вбудованих, званих спеціальними вбудованими . evalце особливий вбудований, readні. Для не спеціальної вбудованої var=value builtinзадає varлише виконання вбудованого, що змушує його вести себе аналогічно, коли виконується зовнішня команда.

commandКоманда може бути використана для видалення спеціального атрибута цих спеціальних вбудованих команд . Що 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функцією, яка використовує або налаштовує $aі потенційно викликає command eval.

Це було справді недоглядом, тому що ksh(на чому ґрунтується специфікація) його не реалізовували (а AT&T kshі zshдосі не роблять), але сьогодні, за винятком цих двох, більшість оболонок реалізують це. Поведінка відрізняється від оболонок, хоча в таких речах, як:

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

хоч. Використання localна оболонках, які підтримують, є більш надійним способом реалізації локальних можливостей.


Як не дивно, IFS=: command eval …встановлюється IFSлише на тривалість eval, як це доручено POSIX, у тире, pdksh та bash, але не в ksh 93u. Незвично бачити, що ksh є незвичайним, невідповідним.
Жил "ТАК - перестань бути злим"

12

Стандартне збереження та відновлення, зняте з програми "Універсальне програмування Unix" від Kernighan та Pike:

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

2
дякую і +1. Так, я знаю про цей варіант, але хотів би знати, чи є варіант "чистішого", якщо ви знаєте, що я маю на увазі
iruvar

Ви можете забити це на одну лінію напівколонами, але я не думаю, що це чистіше. Можливо, було б добре, якби все, що ви хотіли висловити, мав спеціальну синтаксичну підтримку, але тоді нам, мабуть, доведеться навчитися столярній справі чи сумптіну замість кодування;)
msw

9
Це не вдалося відновити $IFSправильно, якщо воно раніше не було встановлено.
Стефан Шазелас

2
Якщо його не встановлено, Баш ставиться до цього $'\t\n'' ', як пояснено тут: wiki.bash-hackers.org/syntax/expansion/…
від

2
@davide, це було б $' \t\n'. простір повинен бути першим, як це було використано "$*". Зауважте, що це однаково у всіх оболонках Борна.
Стефан Шазелас

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.
Стефан Шазелас

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 вийшов із сфери застосування.

Набагато кращим рішенням ІМХО є відмовитися від зміни або покладання на IFS на глобальному / локальному рівні. Натомість, породжуйте нову оболонку та поспіль з IFS. Наприклад, якщо ви повинні викликати func () в main () наступним чином, передаючи масив у вигляді рядка із зворотним подільником косої риски:

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

... що зміна IFS не відображатиметься у функції func (). Масив буде передано у вигляді рядка:

ick\blick\flick

... але всередині func () IFS все ще буде "/" (як встановлено в main ()), якщо не буде змінено локально у func ().

Більше інформації про виділення змін до IFS можна переглянути за наступними посиланнями:

Як перетворити змінну масиву bash в рядок, розмежований новими рядками?

Bash рядок для масиву з IFS

Підказки та підказки щодо загального програмування скриптів оболонки - Див. "ПРИМІТКА про використання під оболонок ..."


цікаво дійсно ...
iruvar

"Bash рядок для масиву IFS" IFS=$'\n' declare -a astr=(...)ідеально дякую!
Сила Водолія

1

Цей фрагмент із запитання:

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

є інтерпретацією як два окремих глобальних змінних призначення, оцінених зліва направо, і еквівалентно:

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

або

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

Це пояснює і те, чому глобальний IFSбув модифікований, і чому розбиття слів $strна елементи масиву виконували за допомогою нового значення IFS.

Можливо, ви б спокусилися використати допоміжну оболонку, щоб обмежити ефект 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 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(ака 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. Однак це рішення не розрізняє скидання 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.