Це розумний підхід до "резервного копіювання" змінної $ IFS?


19

Я завжди дійсно вагаюся з $IFSтим, що це - глобалізація.

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

Тому я вважаю, що це може бути краще, ніж нічого, якщо я спробую "зберегти" початковий вміст $IFSдо іншої змінної, а потім відновити його негайно після того, як я $IFSщось використовую.

Це практично? Або це по суті безглуздо, і я повинен просто безпосередньо IFSповернутися до того, що потрібно для подальшого використання?


Чому це не було б практично?
Братчлі

Тому що невміння IFS добре зробить цю роботу.
llua

1
Для тих, хто говорить, що невміння IFS буде добре працювати, майте на увазі, що це ситуаційно: stackoverflow.com/questions/39545837/… . На мій досвід, найкраще встановити IFS вручну за замовчуванням для вашого інтерпретатора оболонок, а саме, $' \t\n'якщо ви використовуєте bash. unset $IFSпросто не завжди відновлює його до того, що ви очікували б за замовчуванням.
Даррель Холт

Відповіді:


9

Ви можете зберегти та призначити IFS за потреби. У цьому немає нічого поганого. Не рідкість зберегти його значення для відновлення після тимчасової, швидкої модифікації, як, наприклад, ваш присвоєння масиву.

Як згадує @llua у своєму коментарі до вашого запитання, просто скасування IFS відновить поведінку за замовчуванням, рівнозначну призначенню пробілу-вкладки-нового рядка.

Це варто врахувати , як це може бути більш проблематичним НЕ в явному вигляді включено / вимкнено КСФ , ніж зробити це.

З видання POSIX 2013, 2.5.3 змінних оболонок :

Реалізація може ігнорувати значення IFS у навколишньому середовищі або відсутність IFS з навколишнього середовища під час виклику оболонки, і в цьому випадку оболонка повинна встановити IFS <space> <tab> <newline>, коли вона викликається .

Оболонка, сумісна з POSIX, викликає оболонку може або не може успадковувати IFS із свого оточення. З цього випливає:

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

Примітка. Важливо розуміти, що для цього обговорення слово "викликається" має особливе значення. Оболонка викликається лише тоді, коли вона прямо називається, використовуючи її ім'я (включаючи #!/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=:, тоді як тире та busbox це шанують.

$ 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, якщо є якісь котируються розширення параметрів, не readцитовані підстановки команд, нецитовані арифметичні розширення, s або подвійні цитування посилань на $*. Цей список знаходиться у верхній частині моєї голови, тому він може бути не вичерпним (особливо якщо розглядати POSIX-розширення сучасних оболонок).
Босоніж ІО

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не вдасться зняти IFS, якщо він був оголошений локальним у батьківському контексті (контексті функції), а не у поточному контексті.
Стефан Шазелас

5

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

Ти можеш:

  • встановити IFS для одного виклику:

    IFS=value command_or_function

    або

  • встановити IFS всередині підзагорта:

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

Приклади

  • Щоб отримати рядок з комою з масиву:

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

    Примітка. -Це лише захищати порожній масив від set -uнадання значення за замовчуванням, коли його не встановлено (у цьому випадку значенням є порожній рядок) .

    IFSМодифікація може бути застосована тільки всередині подоболочки породжені $() підстановки команд . Це пояснюється тим, що в підпакеті є копії змінних оболонки, що викликає, і тому вони можуть читати їх значення, але будь-які зміни, виконані підшалі, впливають лише на копію, а не на батьківську зміну.

    Ви також можете задуматися: чому б не пропустити нижню частину корпусу і просто зробити це:

    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 ''Або -d ""розповідає читати тільки зупинити свій перший виклик на NULLбайт, замість звичайного перекладу рядка.

  • Для розділення $PATHуздовж його :роздільників:

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

    Цей приклад є чисто показовим. У загальному випадку, коли ви розбиваєтесь на роздільник, можливо, окремі поля містять (уникнуту версію) цього роздільника. Подумайте про те, щоб спробувати прочитати рядок .csvфайлу, стовпці якого можуть самі містити коми (уникнути чи якось цитувати). Вищенаведений фрагмент не працюватиме, як призначено для таких випадків.

    Це означає, що ви навряд чи зустрінетесь із такими :шляхами, що містять у собі всередині $PATH. Хоча дозволено містити імена шляхів UNIX / Linux :, схоже, що 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 @ Greg's )

Я не можу сказати, що я розумію, щоб розділити $ PATH уздовж його: роздільники, припускаючи, що жоден з компонентів не містить речення : себе . Як може містити компоненти , :коли :це роздільник?
Стефан Шазелас

@ StéphaneChazelas Ну, :це дійсний символ, який використовується в імені файлу в більшості файлових систем UNIX / Linux, тому цілком можливо мати каталог, який містить ім'я :. Можливо, в деяких оболонках є можливість вийти :в PATH, використовуючи щось на зразок \:, і тоді ви побачите стовпці, які не є фактичними роздільниками (схоже, bash не дозволяє такі втечі. Функція низького рівня, яка використовується під час ітерації $PATHпросто для пошуку :в рядок C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls

Я переглянув відповідь, щоб, сподіваюся, зробити більш чітким $PATHприклад розбиття :.
sls

1
Ласкаво просимо до SO! Дякую за таку глибоку відповідь :)
Стівен Лу,

1

Це практично? Або це по суті безглуздо, і я повинен просто прямо повернути IFS до того, що потрібно для подальшого використання?

Навіщо ризикувати встановленням друку IFS, $' \t\n'коли все, що вам потрібно зробити

OIFS=$IFS
do_your_thing
IFS=$OIFS

Крім того, ви можете зателефонувати в допоміжну оболонку, якщо вам не потрібні змінні, встановлені / змінені в межах:

( IFS=:; do_your_thing; )

Це небезпечно, оскільки воно не працює, якщо IFSйого спочатку не було.
Жил "ТАК - перестань бути злим"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.