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
один раз для каждого файла.
Объединение отлично упомянутых ресурсов из ответа @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]