В чем разница между IFS= и IFS=$'\n' в bash

So the next best explanation seems to be that every white-space separated string is treated as a separate argument, without any regards to quoting. Is this correct?

Да, см., например,.https://mywiki.wooledge.org/WordSplittingи Почему мой сценарий оболочки забивается пробелами или другими специальными символами? и Когда необходимо двойное -цитирование?

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

And if so, why do backticks have this strange behaviour? I guess this is not what we would want most of the time...

Что ж, странность относительна. И то, что хочется в одном случае, может быть совсем не тем, чего хотят в другом.

Но рассмотрим нечто подобное:

a="blah blah"
somecmd -f "$a"

Это работает следующим образом: somecmdполучает в качестве аргумента строку, содержащуюся в переменной a, независимо от того, что она содержит . Это похоже на то, как это работает в «настоящих» языках программирования, скажем subprocess.call(["somecmd", "-f", a])в Python. Простой, чистый и полностью безопасный :никакие специальные символы в переменной не могут все испортить.

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

echo "Please enter a filename: "
read -r a
somecmd -f "$a"

Если результат расширений обрабатывался на кавычки, то нельзя было вводить Don't stop me now.mp3в качестве имени файла, так как там непарная кавычка.

Кроме того, должны ли результаты всех расширений обрабатываться и для дальнейших расширений? Установка aна $(rm -rf $HOME).txtприведет к довольно неприятным последствиям. Обратите внимание, что это совершенно правильное имя файла, поэтому оно может появиться в результате глобуса, такого как *.txt.

Я знаю, это немного преувеличено, поскольку мы могли бы предложить, чтобы после расширения обрабатывались только кавычки и escape-последовательности, а не какие-либо дальнейшие расширения. Непарные одинарные -кавычки по-прежнему будут проблемой, а $(find -printf "\"%p\"")по-прежнему не будет работать для имен файлов, содержащих двойные -кавычки.

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


Но вы правы, это означает, что не существует очевидного прямого способа получить список строк из findв оболочку. На самом деле это то, что вам действительно нужно, список строк, например sys.argvв Python. Не цитаты.

Вот что вы можете сделать:

find -print0 | xargs -0./test.py

-print0просит findпечатать имена файлов с байтом NUL в качестве разделителя (вместо новой строки ), а -0говорит xargsожидать именно этого. Это работает, поскольку байт NUL — это единственное, что не может содержаться в имени файла. -print0и -0можно найти как минимум в GNU и FreeBSD.

Или, в Баше:

mapfile -d '' files < <(find -print0)
./test.py "${files[@]}"

Это те же строки, разделенные NUL -, которые используются с подстановкой процесса и массивом.

Или, в Bash (сshopt -s globstar)и другими, которые имеют аналогичную функцию, и если вам не нужно фильтровать на основе чего-либо, кроме имени файла:

shopt -s globstar
./test.py./testdata/**

**похож на *, только рекурсивный.

Или со стандартными инструментами:

find -exec./test.py {} +

Это позволяет обойти всю проблему, попросив findзапустить саму test.py, без передачи списка имен файлов куда-либо еще. Однако не помогает, если вам действительно нужно где-то хранить список. Обратите внимание на +в конце, -exec./test.py {} \;будет запускать test.pyодин раз для каждого файла.

6
10.11.2021, 08:51
1 ответ

Объединение отлично упомянутых ресурсов из ответа @Giles, @chorobaкомментария и ответов на другой вопрос . Я собрал следующие примеры кода, чтобы проиллюстрировать различия:


IFS(aka Internal Field Separator )определяет встроенные разделители (допускается несколько символов, порядок не имеет значения ). По умолчанию это IFS=$' \t\n'. Это имеет значение только в том случае, если readзадано несколько переменных целей.

Аргумент

read-dуказывает разделитель строк (принимается только первый символ ). По умолчанию это -d $'\n'.

Таким образом,

# IFS=, -d $'\n', with tab separated fields, across two lines
echo $'a\tb\tc\nz\tx\ty' | while IFS= read -rd $'\t' a b c; do echo "[$a] [$b] [$c]"; done
# [a] [] []
# [b] [] []
# [c
# z] [] []
# [x] [] []

# IFS=tab, with tab separated fields, across two lines
echo $'a\tb\tc\nz\tx\ty' | while IFS=$'\t' read -r a b c; do echo "[$a] [$b] [$c]"; done
# [a] [b] [c]
# [z] [x] [y]

# IFS=tab, with tab separated fields, across two lines, with only a single variable target
echo $'a\tb\tc\nz\tx\ty' | while IFS=$'\t' read -r a; do echo "[$a]"; done
# [a    b   c]
# [z    x   y]

# IFS=tab, with space and tab separated fields, across two lines
echo $'a b\tc\nz\tx y' | while IFS=$'\t' read -r a b c; do echo "[$a] [$b] [$c]"; done
# [a b] [c] []
# [z] [x y] []

# IFS=tab+space, with space and tab separated fields, across two lines
echo $'a b\tc\nz\tx y' | while IFS=$'\t ' read -r a b c; do echo "[$a] [$b] [$c]"; done
# [a] [b] [c]
# [z] [x] [y]

# IFS=newline, -d '', with space and tab separated fields, across two lines
echo $'a b\tc\nz\tx y' | while IFS=$'\n' read -rd '' a b c; do echo "[$a] [$b] [$c]"; done
# outputs nothing, as no delimiter means no lines for inline splitting

# IFS=newline, -d '', with space and tab separated fields, across two lines, with trailing null character
printf 'a b\tc\nz\tx y\0' | while IFS=$'\n' read -rd '' a b c; do echo "[$a] [$b] [$c]"; done
# outputs a single line, with two newline separated fields:
# [a b  c] [z   x y] []

# IFS=newline, -d $'\0', with space and tab separated fields, across two lines, with trailing null character
printf 'a b\tc\nz\tx y\0' | while IFS=$'\n' read -rd $'\0' a b c; do echo "[$a] [$b] [$c]"; done
# outputs a single line, with two newline separated fields:
# [a b  c] [z   x y] []

Таким образом,

  • IFSразбивает «поля» по «линии», это «встроенный» сплиттер
  • -dразделяет "линии", это разделитель "линий"
  • настроить IFS, чтобы настроить разделяющие «поля»
  • настроить -dдля настройки того, что разделяет «линии»

Один вариант использования, когда -dимеет значение,читает каждое поле отдельно, в определенном порядке:

echo $'a b\tc\nz\tx y' | {
    read -rd ' ' a
    echo "a=[$a]"
    read -rd $'\t' b
    echo "b=[$b]"
    read -rd $'\n' c
    echo "c=[$c]"
    read -rd $'\t' z
    echo "z=[$z]"
    read -rd $' ' x
    echo "x=[$x]"
    read -rd $'\n' y
    echo "y=[$y]"
}
# a=[a]
# b=[b]
# c=[c]
# z=[z]
# x=[x]
# y=[y]

Таким образом,

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

Ответ @Gilesохватывает IFSвне контекста read.

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

cd "$(mktemp -d)" || exit 1
touch 'before-space after-space.txt'
touch 'no-space.txt'

# using arrays
# results in correct fields for selection
mapfile -t list < <(ls -1)
select node in "${list[@]}"; do
    echo "via mapfile, [$node]"
    break
done
echo
# outputs:
# 1) before-space after-space.txt
# 2) no-space.txt
# #? 1
# via mapfile, [before-space after-space.txt]

# using word splitting with default `IFS`
# results in mangled fields for selection
select node in $(ls -1); do
    echo "IFS=default [$node]"
    break
done
echo
# outputs:
# 1) before-space
# 2) after-space.txt
# 3) no-space.txt
# #? 1
# IFS=default [before-space]

# using word splitting with `IFS=$'\n'`
# results in the correct fields for selection
IFS=$'\n'
select node in $(ls -1); do
    echo "IFS=newline [$node]"
    break
done
echo
# outputs:
# 1) before-space after-space.txt
# 2) no-space.txt
# #? 1
# IFS=newline [before-space after-space.txt]

# using word splitting with `IFS=`
# results in a jumbled field for selection
IFS=
select node in $(ls -1); do
    echo "IFS= [$node]"
    break
done
echo
# outputs:
# 1) before-space after-space.txt
# no-space.txt
# #? 1
# IFS= [before-space after-space.txt
# no-space.txt]
3
10.11.2021, 10:18

Теги

Похожие вопросы