Всегда используйте двойные кавычки подстановок переменных и команд замен: "$foo"
,"$(foo)"
Если вы используете без $foo
кавычек, ваш сценарий захлебнется вводом или параметрами (или выводом команды, с $(foo)
), содержащими пробел или \[*?
.
Там вы можете перестать читать. Ну, хорошо, вот еще несколько:
read
- Чтобы читать входные данные построчно с помощью read
встроенной функции, используйтеwhile IFS= read -r line; do …
Plain, чтобы read
обрабатывать обратную косую черту и пробелы специально.
xargs
- Избегайтеxargs
. Если вы должны использовать xargs
, сделайте это xargs -0
. Вместо того find … | xargs
, чтобы предпочестьfind … -exec …
.
xargs
обращается с пробелами и персонажами \"'
специально.
Этот ответ относится к оболочкам Bourne / POSIX-стиле ( sh
, ash
, dash
, bash
, ksh
, mksh
, yash
...). Пользователи Zsh должны пропустить это и прочитать конец. Когда необходимо двойное цитирование? вместо. Если вы хотите получить все, что нужно, читайте стандарт или руководство по вашей оболочке.
Обратите внимание, что приведенные ниже пояснения содержат несколько приближений (утверждения, которые верны в большинстве случаев, но могут зависеть от окружающего контекста или конфигурации).
Зачем мне писать "$foo"
? Что происходит без кавычек?
$foo
не означает «принять значение переменной foo
». Это означает что-то гораздо более сложное:
- Сначала возьмите значение переменной.
- Разделение полей: обработайте это значение как список полей, разделенных пробелами, и создайте получившийся список. Например, если переменная содержит
foo * bar
то результатом этого шага является список 3-элемент foo
, *
, bar
.
- Генерация имени файла: обрабатывайте каждое поле как глобус, то есть как шаблон с подстановочными знаками, и заменяйте его списком имен файлов, соответствующих этому шаблону. Если шаблон не соответствует ни одному файлу, он остается неизменным. В нашем примере это приводит к списку, который содержит
foo
список файлов в текущем каталоге и, наконец, список bar
. Если текущий каталог пуст, результат foo
, *
, bar
.
Обратите внимание, что результатом является список строк. В синтаксисе оболочки есть два контекста: контекст списка и строковый контекст. Разделение полей и генерация файлов происходят только в контексте списка, но это происходит в большинстве случаев. Двойные кавычки отделяют строковый контекст: вся строка в двойных кавычках представляет собой одну строку, которую нельзя разделять. (Исключение: "$@"
расширение до списка позиционных параметров, например "$@"
, эквивалентно, "$1" "$2" "$3"
если есть три позиционных параметра. См. В чем разница между $ * и $ @? )
То же самое происходит с подстановкой команд с $(foo)
или `foo`
. Кстати, не используйте `foo`
: его правила цитирования странные и непереносимые, и все современные оболочки поддерживают, $(foo)
что абсолютно эквивалентно, за исключением наличия интуитивно понятных правил цитирования.
Вывод арифметической подстановки также претерпевает те же расширения, но это обычно не проблема, поскольку он содержит только нерасширяемые символы (при условии, что IFS
они не содержат цифр или -
).
См. Когда необходимо двойное цитирование? для получения более подробной информации о случаях, когда вы можете опустить цитаты.
Если вы не хотите, чтобы все это происходило, просто не забывайте всегда использовать двойные кавычки вокруг подстановок переменных и команд. Будьте осторожны: пропуск цитат может привести не только к ошибкам, но и к дырам в безопасности .
Как мне обработать список имен файлов?
Если вы пишете myfiles="file1 file2"
с пробелами для разделения файлов, это не может работать с именами файлов, содержащими пробелы. Имена файлов Unix могут содержать любой символ, кроме /
(который всегда является разделителем каталогов) и нулевых байтов (которые нельзя использовать в сценариях оболочки с большинством оболочек).
Та же проблема с myfiles=*.txt; … process $myfiles
. Когда вы делаете это, переменная myfiles
содержит 5-символьную строку *.txt
, и когда вы пишете $myfiles
, подстановочный знак раскрывается. Этот пример будет работать, пока вы не измените свой сценарий на myfiles="$someprefix*.txt"; … process $myfiles
. Если someprefix
установлено значение final report
, это не будет работать.
Чтобы обработать список любого типа (например, имена файлов), поместите его в массив. Для этого требуется mksh, ksh93, yash или bash (или zsh, у которого нет всех этих проблем с цитированием); простая оболочка POSIX (например, ash или dash) не имеет переменных массива.
myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Ksh88 имеет переменные-массивы с другим синтаксисом присваивания set -A myfiles "someprefix"*.txt
(смотрите переменную присваивания в другой среде ksh, если вам нужна переносимость ksh88 / bash). Оболочки в стиле Bourne / POSIX имеют единый массив - массив позиционных параметров, "$@"
который вы устанавливаете set
и который является локальным для функции:
set -- "$someprefix"*.txt
process -- "$@"
Как насчет имен файлов, которые начинаются с -
?
Обратите внимание на то, что имена файлов могут начинаться с -
(тире / минус), который большинство команд интерпретирует как обозначение параметра. Если у вас есть имя файла, которое начинается с переменной части, обязательно --
перед ним, как в приведенном выше фрагменте. Это указывает команде, что она достигла конца опций, поэтому все, что после этого является именем файла, даже если оно начинается с -
.
Кроме того, вы можете убедиться, что имена ваших файлов начинаются с символа, отличного от -
. Абсолютные имена файлов начинаются с /
, и вы можете добавить ./
в начале относительные имена. Следующий фрагмент кода превращает содержимое переменной f
в «безопасный» способ ссылки на тот же файл, с которого гарантированно не начинаться -
.
case "$f" in -*) "f=./$f";; esac
Последнее замечание по этой теме, помните, что некоторые команды интерпретируют -
как означающие стандартный ввод или стандартный вывод, даже после --
. Если вам нужно сослаться на фактический файл с именем -
или если вы вызываете такую программу и не хотите, чтобы она читала из stdin или записывала в stdout, не забудьте переписать, -
как указано выше. См. В чем разница между "du -sh *" и "du -sh ./*"? для дальнейшего обсуждения.
Как мне сохранить команду в переменной?
«Команда» может означать три вещи: имя команды (имя в виде исполняемого файла, с полным путем или без него, или имя функции, встроенного или псевдонима), имя команды с аргументами или фрагмент кода оболочки. Соответственно есть разные способы хранения их в переменной.
Если у вас есть имя команды, просто сохраните его и используйте переменную с двойными кавычками, как обычно.
command_path="$1"
…
"$command_path" --option --message="hello world"
Если у вас есть команда с аргументами, проблема та же, что и со списком имен файлов выше: это список строк, а не строка. Вы не можете просто вставить аргументы в одну строку с пробелами между ними, потому что если вы это сделаете, вы не сможете определить разницу между пробелами, которые являются частью аргументов, и пробелами, которые разделяют аргументы. Если в вашей оболочке есть массивы, вы можете использовать их.
cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
Что делать, если вы используете оболочку без массивов? Вы все еще можете использовать позиционные параметры, если не возражаете против их изменения.
set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"
Что если вам нужно сохранить сложную команду оболочки, например, с перенаправлениями, каналами и т. Д.? Или если вы не хотите изменять позиционные параметры? Затем вы можете построить строку, содержащую команду, и использовать eval
встроенную.
code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"
Обратите внимание на вложенные кавычки в определении code
: одинарные кавычки '…'
отделяют строковый литерал, так что значением переменной code
является строка /path/to/executable --option --message="hello world" -- /path/to/file1
. eval
Встроенный говорит оболочку , чтобы разобрать строку , переданную в качестве аргумента , как если бы он появился в сценарии, так что в этот момент котировка и трубы разобраны и т.д.
Использование eval
сложно. Подумайте внимательно о том, что когда анализируется. В частности, вы не можете просто вставить имя файла в код: вам нужно заключить его в кавычки, как если бы оно было в файле исходного кода. Там нет прямого способа сделать это. Что - то вроде code="$code $filename"
перерывов , если имя файла содержит какой - либо оболочки специальных символов (пробелы, $
, ;
, |
, <
, >
и т.д.). code="$code \"$filename\""
все еще ломается "$\`
. Даже code="$code '$filename'"
ломается, если имя файла содержит '
. Есть два решения.
Добавьте слой кавычек вокруг имени файла. Самый простой способ сделать это - добавить одинарные кавычки вокруг него и заменить одинарные кавычки на '\''
.
quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
code="$code '${quoted_filename%.}'"
Сохраняйте расширение переменной внутри кода, чтобы оно просматривалось при оценке кода, а не при построении фрагмента кода. Это проще, но работает, только если переменная все еще присутствует с тем же значением во время выполнения кода, а не, например, если код встроен в цикл.
code="$code \"\$filename\""
Наконец, вам действительно нужна переменная, содержащая код? Самый естественный способ дать имя блоку кода - это определить функцию.
Что с тобой read
?
Без -r
, read
позволяет продолжить строки - это одна логическая строка ввода:
hello \
world
read
разбивает строку ввода на поля, разделенные символами в $IFS
(без -r
, обратный слеш также экранирует их). Например, если вход представляет собой строку, содержащую три слова, тогда read first second third
устанавливается first
первое слово ввода, second
второе слово и third
третье слово. Если есть больше слов, последняя переменная содержит все, что осталось после установки предыдущих. Ведущие и конечные пробелы обрезаются.
Установка IFS
на пустую строку позволяет избежать обрезки. Посмотрите, почему `while IFS = read` используется так часто, вместо` IFS =; пока читаешь? для более длинного объяснения.
Что не так с xargs
?
Формат ввода xargs
- строки, разделенные пробелами, которые могут быть заключены в одинарные или двойные кавычки. Ни один стандартный инструмент не выводит этот формат.
Ввод в xargs -L1
или xargs -l
почти список строк, но не совсем - если в конце строки есть пробел, следующая строка является продолжением.
Вы можете использовать xargs -0
там, где это применимо (и где доступно: GNU (Linux, Cygwin), BusyBox, BSD, OSX, но его нет в POSIX). Это безопасно, потому что нулевые байты не могут появляться в большинстве данных, в частности в именах файлов. Чтобы создать разделенный нулями список имен файлов, используйте find … -print0
(или вы можете использовать, find … -exec …
как описано ниже).
Как мне обработать найденные файлы find
?
find … -exec some_command a_parameter another_parameter {} +
some_command
должна быть внешней командой, это не может быть функция оболочки или псевдоним. Если вам нужно вызвать оболочку для обработки файлов, вызывайте sh
явно.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
У меня есть другой вопрос
Просмотрите тег цитирования на этом сайте, или shell или shell-script . (Нажмите «узнать больше…», чтобы увидеть общие советы и отобранный список общих вопросов.) Если вы искали и не нашли ответа, задайте вопрос .
shellcheck
помочь вам улучшить качество ваших программ.