Как правильно назначить временные переменные Bash для каждой команды? - PullRequest
3 голосов
/ 30 апреля 2019

Похоже, что Bash ведет себя непредсказуемо в отношении временного назначения переменных для каждой команды, особенно при IFS.

Я часто присваиваю IFS временное значение вместе с командой read. Я хотел бы использовать ту же механику для настройки вывода, но в настоящее время прибегаю к функции или подоболочке, чтобы содержать присвоение переменной.

$ while IFS=, read -a A; do
>   echo "${A[@]:1:2}"                # control (undesirable)
> done <<< alpha,bravo,charlie
bravo charlie

$ while IFS=, read -a A; do
>   IFS=, echo "${A[*]:1:2}"          # desired solution (failure)
> done <<< alpha,bravo,charlie
bravo charlie

$ perlJoin(){ local IFS="$1"; shift; echo "$*"; }
$ while IFS=, read -a A; do
>   perlJoin , "${A[@]:1:2}"          # function with local variable (success)
> done <<< alpha,bravo,charlie
bravo,charlie

$ while IFS=, read -a A; do
>   (IFS=,; echo "${A[*]:1:2}")       # assignment within subshell (success)
> done <<< alpha,bravo,charlie
bravo,charlie

Если второе назначение в следующем блоке не влияет на среду команды и не генерирует ошибку, то для чего это нужно?

$ foo=bar
$ foo=qux echo $foo
bar

Ответы [ 3 ]

3 голосов
/ 30 апреля 2019
$ foo=bar
$ foo=qux echo $foo
bar

Это обычный bash gotcha - и https://www.shellcheck.net/ ловит его:


foo=qux echo $foo
^-- SC2097: This assignment is only seen by the forked process.
             ^-- SC2098: This expansion will not see the mentioned assignment.

Проблема в том, что первый foo=bar устанавливает переменную bash, а не переменную окружения. Затем встроенный синтаксис foo=qux используется для установки переменной среды для echo, однако echo фактически никогда не смотрит на эту переменную. Вместо этого $foo распознается как переменная bash и заменяется на bar.

Итак, вернемся к вашему основному вопросу: вы были там с последней попыткой использования подоболочки - за исключением того, что вам не нужен подоболочек:

while IFS=, read -a A; do
  IFS=,; echo "${A[*]:1:2}"
done <<< alpha,bravo,charlie

выходы:

bravo,charlie

Для полноты, вот последний пример, который читает в несколько строк и использует другой выходной разделитель, чтобы продемонстрировать, что различные назначения IFS не давят друг на друга:

while IFS=, read -a A; do
  IFS=:; echo "${A[*]:1:2}"
done < <(echo -e 'alpha,bravo,charlie\nfoo,bar,baz')

выходы:

bravo:charlie
bar:baz
2 голосов
/ 30 апреля 2019

Ответ немного проще, чем другие ответы:

$ foo=bar
$ foo=qux echo $foo
bar

Мы видим "бар", потому что оболочка расширяется $foo до настройки foo=qux

Простое расширение команд - здесь многое предстоит пройти, так что терпите меня ...

Когда выполняется простая команда, оболочка выполняет следующие расширения, назначения и перенаправления слева направо.

  1. Слова, помеченные синтаксическим анализатором как назначения переменных (те, которые предшествуют имени команды) и перенаправления , сохраняются для последующей обработки .
  2. Слова, которые не являются переменными назначениями или перенаправлениями, раскрыты (см. Расширения оболочки ). Если после раскрытия остаются какие-либо слова, первым словом считается имя команды, а остальными словами - аргументы.
  3. Перенаправления выполняются, как описано выше (см. Перенаправления).
  4. Текст после ‘=’ в каждом назначении переменной подвергается расширению тильды, расширению параметров, подстановке команд, арифметическому расширению и удалению кавычек перед присвоением переменной.

Если имя команды не найдено, назначения переменных влияют на текущую среду оболочки. В противном случае переменные добавляются в среду выполняемой команды и не влияют на текущую среду оболочки . Если какое-либо из назначений пытается присвоить значение переменной только для чтения, возникает ошибка, и команда завершается с ненулевым состоянием.

Если имя команды не найдено, перенаправления выполняются, но не влияют на текущую среду оболочки. Ошибка перенаправления приводит к выходу команды с ненулевым статусом.

Если после раскрытия остается имя команды, выполнение продолжается , как описано ниже. В противном случае команда завершается. Если одно из расширений содержало подстановку команд, статусом выхода команды является статус выхода последней выполненной подстановки команды. Если не было подстановок команд, команда завершается со статусом ноль.

Итак:

  • оболочка видит foo=qux и сохраняет это на потом
  • оболочка видит $foo и расширяет ее до "бара"
  • тогда у нас теперь есть: foo=qux echo bar

Как только вы действительно поймете порядок, в котором работает bash, многие тайны исчезнут.

1 голос
/ 30 апреля 2019

Краткий ответ: последствия изменения IFS сложны и трудны для понимания, и их лучше избегать, за исключением нескольких четко определенных идиом (IFS=, read ... - это одна из тех фраз, которые я считаю приемлемыми).

Длинный ответ: есть несколько вещей, которые вы должны иметь в виду, чтобы понять результаты, которые вы видите от изменений в IFS:

  • Использование IFS=something в качествепрефикс команды изменяется IFS только для выполнения этой одной команды .В частности, это не влияет на то, как оболочка анализирует аргументы, передаваемые этой команде;это управляется значением оболочки IFS, а не тем, которое используется для выполнения команды.

  • Некоторые команды обращают внимание на значение IFS, с которым они выполняются (например,read), но другие этого не делают (например, echo).

Учитывая вышесказанное, IFS=, read -a A делает то, что вы ожидаете, он делит свой ввод на ",":

$ IFS=, read -a A <<<"alpha,bravo,charlie"
$ declare -p A
declare -a A='([0]="alpha" [1]="bravo" [2]="charlie")'

Но echo не обращает внимания;он всегда ставит пробелы между передаваемыми аргументами, поэтому использование IFS=something в качестве префикса к нему никак не влияет:

$ echo alpha bravo
alpha bravo
$ IFS=, echo alpha bravo
alpha bravo

Так что, когда вы используете IFS=, echo "${A[*]:1:2}", это эквивалентно просто echo "${A[*]:1:2}"и поскольку определение оболочки IFS начинается с пробела, она помещает элементы A вместе с пробелами между ними.Таким образом, это эквивалентно выполнению IFS=, echo "alpha bravo".

С другой стороны, IFS=,; echo "${A[*]:1:2}" меняет определение оболочки на IFS, поэтому оно влияет на то, как оболочка соединяет элементы, так что получается эквивалентноIFS=, echo "alpha,bravo".К сожалению, с этого момента это также влияет на все остальное, поэтому вам нужно либо изолировать его до подоболочки, либо впоследствии вернуть его в нормальное состояние.

Просто для полноты, вот пара других версий, которые этого не делаютwork:

$ IFS=,; echo "${A[@]:1:2}"
bravo charlie

В этом случае [@] указывает оболочке обрабатывать каждый элемент массива как отдельный аргумент, поэтому для объединения их остается echo, и он игнорирует IFS и всегда использует пробелы.

$ IFS=,; echo "${A[@]:1:2}"
bravo charlie

Как насчет этого:

$ IFS=,; echo ${A[*]:1:2}
bravo charlie

В этом случае [*] говорит оболочке смешивать все элементы вместе с первым символомIFS между ними, давая bravo,charlie.Но это не в двойных кавычках, поэтому оболочка немедленно разделяет его на «,», снова разделяя его на отдельные аргументы (а затем echo объединяет их с пробелами, как всегда).

Если выЕсли вы хотите изменить определение оболочки на IFS, не изолируя его от подоболочки, есть несколько вариантов, чтобы изменить его и впоследствии установить обратно.В bash вы можете вернуть его в нормальное состояние следующим образом:

$ IFS=,
$ while read -a A; do    # Note: IFS change not needed here; it's already changed
> echo "${A[*]:1:2}"
> done <<<alpha,bravo,charlie
bravo,charlie
$ IFS=$' \t\n'

Но синтаксис $'...' доступен не во всех оболочках;если вам нужна переносимость, лучше использовать буквенные символы:

IFS=' 
'        # You can't see it, but there's a literal space and tab after the first '

Некоторые люди предпочитают использовать unset IFS, что просто заставляет оболочку работать по умолчанию, что в значительной степени аналогично IFSопределяется обычным способом.

... но если IFS был изменен в каком-то более крупном контексте, и вы не хотите это испортить, вам нужно сохранить его, а затем установить обратно.Если он был изменен в обычном режиме, это сработает:

saveIFS=$IFS
...
IFS=$saveIFS

... но если кто-то посчитает целесообразным использовать unset IFS, это определит его как пустое, что даст странные результаты.Таким образом, вы можете использовать этот подход или unset подход, но не оба.Если вы хотите сделать это устойчивым к конфликту unset, вы можете использовать что-то вроде этого в bash:

saveIFS=${IFS:-$' \t\n'}

... или для переносимости, отключите $' ' и используйте буквальное пространство +tab + newline:

saveIFS=${IFS:- 
}                # Again, there's an invisible space and tab at the end of the first line

В общем, это много путаницы, полной ловушек для неосторожных.Я рекомендую избегать этого всякий раз, когда это возможно.

...