Является ли это нормальным подходом для «резервного копирования» переменной $ IFS?

19

Я всегда очень не решаюсь возиться с этим, $IFSпотому что это заглушает глобальное.

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

Поэтому я полагаю, что может быть лучше, чем ничего, если я попытаюсь «сохранить» начальное содержимое $IFSв другой переменной, а затем восстановить его сразу после того, как я что-то использую $IFS.

Это практично? Или это, по сути, бессмысленно, и я должен просто IFSвернуться к тому, чем он должен быть для его последующего использования?

Стивен Лу
источник
Почему бы это не практично?
Братчли
Потому что отключение IFS будет хорошо работать.
Луа
1
Для тех, кто говорит, что сброс IFS будет работать нормально, имейте в виду, что он ситуативный: stackoverflow.com/questions/39545837/… . По моему опыту, лучше всего установить IFS вручную по умолчанию для вашего интерпретатора оболочки, а именно, $' \t\n'если вы используете bash. unset $IFSпросто не всегда восстанавливать его до того, что вы ожидаете по умолчанию.
Даррел Холт

Ответы:

9

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

Как упоминает @llua в своем комментарии к вашему вопросу, простое отключение IFS восстановит поведение по умолчанию, эквивалентное назначению пробела-табуляции-новой строки.

Стоит подумать о том, как может быть более проблематично не устанавливать / не устанавливать IFS явно, чем делать это.

Из версии POSIX 2013, 2.5.3 Переменные оболочки :

Реализации могут игнорировать значение IFS в среде или отсутствие IFS в среде во время вызова оболочки, и в этом случае оболочка должна установить IFS на <space> <tab> <newline>, когда она вызывается ,

Вызванная POSIX-совместимая оболочка может наследовать или не наследовать IFS из своего окружения. Из этого следует:

  • Переносимый сценарий не может надежно наследовать IFS через среду.
  • Сценарий, который намеревается использовать только поведение разделения по умолчанию (или присоединение, в случае "$*"), но который может выполняться под оболочкой, которая инициализирует IFS из среды, должен явно установить / сбросить IFS, чтобы защитить себя от вторжения в окружающую среду.

NB. Важно понимать, что для этого обсуждения слово «вызванный» имеет особое значение. Оболочка вызывается только тогда, когда она вызывается явно, используя свое имя (включая #!/path/to/shellшебанг). Подоболочка - например, которая может быть создана $(...)или cmd1 || cmd2 &- не является вызванной оболочкой, и ее IFS (вместе с большей частью среды выполнения) идентичен родительскому. Вызванная оболочка устанавливает значение $pid, в то время как подоболочки наследуют его.


Это не просто педантичное разоблачение; в этой области существует фактическое расхождение. Вот краткий скрипт, который тестирует сценарий с использованием нескольких различных оболочек. Он экспортирует измененный IFS (установленный в :) в вызванную оболочку, которая затем печатает свой IFS по умолчанию.

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFS обычно не помечен для экспорта, но, если это так, обратите внимание, как bash, ksh93 и mksh игнорируют окружение IFS=:, в то время как dash и busybox это соблюдают.

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

Некоторая информация о версии:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

Хотя bash, ksh93 и mksh не инициализируют IFS из среды, они реэкспортируют свои модифицированные IFS.

Если по какой-либо причине вам нужно переносить IFS через среду, вы не можете сделать это, используя сам IFS; вам нужно будет присвоить значение другой переменной и пометить эту переменную для экспорта. Затем дети должны будут явно присвоить это значение своим IFS.

Босиком И.О.
источник
Я вижу, поэтому, если я могу перефразировать, возможно, более переносимо явно указывать IFSзначение в большинстве ситуаций, где оно должно использоваться, и поэтому зачастую не очень продуктивно даже пытаться «сохранить» его первоначальное значение.
Стивен Лу
1
Первостепенная проблема заключается в том, что если ваш сценарий использует IFS, он должен явно установить / сбросить IFS, чтобы убедиться, что его значение соответствует желаемому. Как правило, поведение вашего сценария зависит от IFS, если есть какие-либо расширения параметров без кавычек, подстановки команд без кавычек, арифметические расширения без кавычек, reads или ссылки в двойных кавычках $*. Этот список просто не в моей голове, поэтому он может быть не исчерпывающим (особенно при рассмотрении POSIX-расширений современных оболочек).
Босиком IO
10

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

Однако в этом случае не так уж и много.

Почему?:

Кроме того, хранение значения IFS имеет проблему.
Если исходный IFS был не установлен, код IFS="$OldIFS"установит IFS на "", а не на его сброс.

Чтобы фактически сохранить значение IFS (даже если оно не установлено), используйте это:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.
Сообщество
источник
IFS действительно не может быть сброшен. Если вы отключите его, оболочка вернет его к значению по умолчанию. Так что вам не нужно проверять это при сохранении.
filbranden
Учтите , что в bash, unset IFSне в неустановленный МФС , если она была объявлена локальной в родительском контексте (контексте функции) , а не в текущем контексте.
Стефан Шазелас
5

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

Вы можете:

  • установить IFS для одного вызова:

    IFS=value command_or_function

    или

  • установить IFS внутри подоболочки:

    (IFS=value; statement)
    $(IFS=value; statement)

Примеры

  • Чтобы получить разделенную запятыми строку из массива:

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

    Примечание. Предназначено -только для защиты пустого массива set -uпутем предоставления значения по умолчанию, если оно не установлено (в данном случае это значение является пустой строкой) .

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

    Вы также можете подумать: почему бы не пропустить subshell и просто сделать это:

    IFS=, str="${array[*]-}"  # Don't do this!

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

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    Наконец, давайте объясним, почему этот вариант не будет работать:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    Команда echoдействительно будет вызываться с IFSпеременной, установленной в ,, но echoэто не заботится и не используется IFS. Волшебство расширения "${array[*]}"до строки выполняется самой (суб) оболочкой до того, как echoона даже была вызвана.

  • Чтобы прочитать весь файл (который не содержит NULLбайтов) в одну переменную с именем VAR:

    IFS= read -r -d '' VAR < "${filepath}"

    Примечание: IFS=это то же самое, что IFS=""и IFS='', все из которых устанавливают IFS в пустую строку, что очень отличается от unset IFS: если IFSне установлено, поведение всех функциональных возможностей bash, которые используются внутри, IFSточно такое же, как если бы IFSимело значение по умолчанию $' \t\n'.

    Установка IFSпустой строки обеспечивает сохранение начальных и конечных пробелов.

    Оператор -d ''or -d ""сообщает read только для того, чтобы остановить текущий вызов на NULLбайте вместо обычного перевода строки.

  • Чтобы разделить $PATHего по :разделителям:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    Этот пример чисто иллюстративный. В общем случае, когда вы разделяете разделитель, отдельные поля могут содержать (экранированную версию) этот разделитель. Подумайте о попытке прочитать строку .csvфайла, столбцы которого могут содержать запятые (экранированные или заключенные в кавычки каким-либо образом). Приведенный выше фрагмент не будет работать, как предназначено для таких случаев.

    Тем не менее, вы вряд ли встретите такие- :содержащие пути внутри $PATH. Хотя в именах путей UNIX / Linux разрешено содержать a :, кажется, что bash не сможет обработать такие пути в любом случае, если вы попытаетесь добавить их в свой $PATHи сохранить в них исполняемые файлы, так как нет кода для анализа двоеточий / кавычек : исходный код bash 4.4 .

    Наконец, обратите внимание, что фрагмент добавляет завершающий символ новой строки к последнему элементу результирующего массива (как вызвано @ StéphaneChazelas в теперь удаленных комментариях), и что если входные данные являются пустой строкой, выходные данные будут одноэлементными. массив, где элемент будет состоять из символа новой строки ( $'\n').

мотивация

Базовый old_IFS="${IFS}"; command; IFS="${old_IFS}"подход, который касается глобального, IFSбудет работать, как ожидается, для самых простых сценариев. Однако, как только вы добавите любую сложность, она может легко развалиться и вызвать тонкие проблемы:

  • Если commandэто bash-функция, которая также изменяет глобальную IFS(либо непосредственно, либо скрытую от взгляда внутри еще одной вызываемой ею функции) и при этом ошибочно использует ту же глобальную old_IFSпеременную для сохранения / восстановления, вы получаете ошибку.
  • Как отмечено в этом комментарии @Gilles , если исходное состояние IFSбыло не установлено, наивное сохранение и восстановление не будет работать, и даже приведет к прямым сбоям, если обычно (неправильно) используемая set -u(иначе set -o nounset) опция оболочки в силе.
  • Некоторый код оболочки может выполняться асинхронно с основным потоком выполнения, например, с обработчиками сигналов (см. help trap). Если этот код также изменяет глобальный код IFSили предполагает, что он имеет определенное значение, вы можете получить незначительные ошибки.

Вы могли бы разработать более надежную последовательность сохранения / восстановления (такую, как предложенная в этом другом ответе, чтобы избежать некоторых или всех этих проблем. Однако вам придется повторять этот фрагмент шумного стандартного кода везде, где вам временно нужен пользовательский IFS. уменьшает читабельность кода и ремонтопригодность.

Дополнительные соображения для библиотечных сценариев

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

При написании кода библиотеки вы не можете полагаться на IFSкакое-либо конкретное значение (даже не на значение по умолчанию) или даже на его установку вообще. Вместо этого вам нужно явно указать IFSлюбой фрагмент, от которого зависит его поведение IFS.

Если IFSявно задано необходимое значение (даже если это значение по умолчанию) в каждой строке кода, где значение имеет значение, с использованием любого из двух механизмов, описанных в этом ответе, подходит для локализации эффекта, тогда код независимый от глобального государства и избегающий засорения его в целом. Этот подход имеет дополнительное преимущество, заключающееся в том, что он очень понятен для человека, читающего сценарий, который IFSважен именно для этой одной команды / расширения с минимальными текстовыми затратами (по сравнению даже с самым простым сохранением / восстановлением).

На какой код все равно влияет IFS?

К счастью, не так много сценариев, в которых IFSимеет значение (при условии, что вы всегда цитируете свои расширения ):

  • "$*"и "${array[*]}"расширения
  • вызовы readвстроенного таргетинга для нескольких переменных ( read VAR1 VAR2 VAR3) или переменной массива ( read -a ARRAY_VAR_NAME)
  • вызовы readтаргетинга на одну переменную, когда дело доходит до начальных / конечных пробельных символов или непробельных символов, появляющихся в IFS.
  • расщепление слов (например, для расширений без кавычек, которых вы можете избежать, как чумы )
  • некоторые другие менее распространенные сценарии (см .: IFS @ Wreg Грега )
СЛС
источник
Я не могу сказать, что понимаю разделение $ PATH по разделителям: при условии, что ни один из компонентов не содержит предложение a : self. Как компоненты могут содержать, :когда :разделитель?
Стефан Шазелас
@ StéphaneChazelas Хорошо, :это допустимый символ для использования в имени файла в большинстве файловых систем UNIX / Linux, поэтому вполне возможно иметь каталог с именем, содержащим :. Возможно, в некоторых оболочках есть условие для выхода :в PATH с использованием чего-то подобного \:, и тогда вы увидите столбцы, которые не являются фактическими разделителями (кажется, что bash не допускает такое экранирование. Функция низкого уровня, используемая при итерации, $PATHпросто ищет :в строка C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
Слз
Я пересмотрел ответ, чтобы, надеюсь, сделать $PATHпример разделения :более понятным.
Слз
1
Добро пожаловать на ТАК! Спасибо за такой подробный ответ :)
Стивен Лу
1

Это практично? Или это, по сути, бессмысленно, и я должен просто вернуть IFS обратно к тому, чем он должен быть для его последующего использования?

Зачем рисковать настройкой IFS, $' \t\n'когда все, что вам нужно сделать, это

OIFS=$IFS
do_your_thing
IFS=$OIFS

Кроме того, вы можете вызвать subshell, если вам не нужны переменные, установленные / измененные внутри:

( IFS=:; do_your_thing; )
arielCo
источник
Это опасно, потому что не работает, если IFSизначально было не установлено.
Жиль "ТАК - перестать быть злым"