Настройка 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
Iruvar
источник

Ответы:

39

В некоторых оболочках (в том числе bash):

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

(с помощью bash, вы можете опустить, commandесли не в эмуляции sh / POSIX). Но имейте в виду, что при использовании переменных без кавычек вам, как правило, это необходимо set -f, и в большинстве оболочек для этого нет локальной области действия.

С Zsh вы можете сделать:

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

$=PATHзаключается в принудительном разделении слов, которое по умолчанию не выполняется zsh(глобализация при раскрытии переменных также не выполняется, поэтому вам не нужно ничего, set -fкроме эмуляции).

(){...}(или 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.

Теперь это не относится к встроенным функциям или функциям, которые не выполняются . В 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, в dash, pdksh и bash, но не в ksh 93u. Необычно видеть, что ksh нечетный, не соответствует требованиям.
Жиль "ТАК - перестань быть злым"
12

Стандартные сохранения и восстановления, взятые из «Среды программирования Unix» Керниганом и Пайком:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}
MSW
источник
2
спасибо и +1. Да, я знаю об этой опции, но я хотел бы знать, есть ли опция «чище», если вы знаете, что я имею в виду
iruvar
Вы можете вставить его в одну строку с помощью точек с запятой, но я не думаю, что это чище. Было бы хорошо, если бы все, что вы хотели выразить, имело специальную синтаксическую поддержку, но тогда нам, вероятно, пришлось бы изучать столярное дело или sumptin вместо кодирования;)
msw
10
Это не может восстановить $IFSправильно, если он был ранее не установлен.
Стефан Шазелас
2
Если он не установлен, Bash рассматривает его так $'\t\n'' ', как описано здесь: wiki.bash-hackers.org/syntax/expansion/…
davide
2
@ Давид, это было бы $' \t\n' . пространство должно быть первым, как это используется для "$*". Обратите внимание, что это одинаково во всех оболочках типа Борна.
Стефан Шазелас
8

Поместите ваш скрипт в функцию и вызовите эту функцию, передав ей аргументы командной строки. Поскольку IFS определяется локально, его изменения не влияют на глобальный IFS.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"
helpermethod
источник
6

Для этой команды:

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

Существует альтернативное решение: дать первому назначению ( IFS=$'\n') команду для выполнения (функцию):

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

Это поместит IFS в среду для вызова split, но не будет сохранено в текущей среде.

Это также позволяет избежать рискованного использования 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

Советы и рекомендации по общему программированию сценариев оболочки - см. «ПРИМЕЧАНИЕ об использовании вложенных оболочек ...»

markeissler
источник
действительно интересно ...
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
jmd_dk
источник