Или вводное руководство по надежной обработке имени файла и другой передаче строк в сценариях оболочки.
Я написал сценарий оболочки, который работает хорошо большую часть времени. Но он душит некоторые входные данные (например, некоторые имена файлов).
Я столкнулся с такой проблемой, как:
- У меня есть имя файла, содержащее пробел
hello world
, и оно рассматривалось как два отдельных файлаhello
иworld
. - У меня есть строка ввода с двумя последовательными пробелами, и они сократились до одного на входе.
- Начальные и конечные пробелы исчезают из строк ввода.
- Иногда, когда вход содержит один из символов
\[*?
, они заменяются неким текстом, который на самом деле является именем файла. - Во входных данных есть апостроф
'
(или двойная кавычка"
), и после этого все становится странным. - Во входных данных есть обратная косая черта (или: я использую Cygwin, и некоторые из моих имен файлов имеют
\
разделители в стиле Windows ).
Что происходит и как мне это исправить?
shellcheck
помочь вам улучшить качество ваших программ.Ответы:
Всегда используйте двойные кавычки подстановок переменных и команд замен:
"$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) не имеет переменных массива.
Ksh88 имеет переменные-массивы с другим синтаксисом присваивания
set -A myfiles "someprefix"*.txt
(смотрите переменную присваивания в другой среде ksh, если вам нужна переносимость ksh88 / bash). Оболочки в стиле Bourne / POSIX имеют единый массив - массив позиционных параметров,"$@"
который вы устанавливаетеset
и который является локальным для функции:Как насчет имен файлов, которые начинаются с
-
?Обратите внимание на то, что имена файлов могут начинаться с
-
(тире / минус), который большинство команд интерпретирует как обозначение параметра. Если у вас есть имя файла, которое начинается с переменной части, обязательно--
перед ним, как в приведенном выше фрагменте. Это указывает команде, что она достигла конца опций, поэтому все, что после этого является именем файла, даже если оно начинается с-
.Кроме того, вы можете убедиться, что имена ваших файлов начинаются с символа, отличного от
-
. Абсолютные имена файлов начинаются с/
, и вы можете добавить./
в начале относительные имена. Следующий фрагмент кода превращает содержимое переменнойf
в «безопасный» способ ссылки на тот же файл, с которого гарантированно не начинаться-
.Последнее замечание по этой теме, помните, что некоторые команды интерпретируют
-
как означающие стандартный ввод или стандартный вывод, даже после--
. Если вам нужно сослаться на фактический файл с именем-
или если вы вызываете такую программу и не хотите, чтобы она читала из stdin или записывала в stdout, не забудьте переписать,-
как указано выше. См. В чем разница между "du -sh *" и "du -sh ./*"? для дальнейшего обсуждения.Как мне сохранить команду в переменной?
«Команда» может означать три вещи: имя команды (имя в виде исполняемого файла, с полным путем или без него, или имя функции, встроенного или псевдонима), имя команды с аргументами или фрагмент кода оболочки. Соответственно есть разные способы хранения их в переменной.
Если у вас есть имя команды, просто сохраните его и используйте переменную с двойными кавычками, как обычно.
Если у вас есть команда с аргументами, проблема та же, что и со списком имен файлов выше: это список строк, а не строка. Вы не можете просто вставить аргументы в одну строку с пробелами между ними, потому что если вы это сделаете, вы не сможете определить разницу между пробелами, которые являются частью аргументов, и пробелами, которые разделяют аргументы. Если в вашей оболочке есть массивы, вы можете использовать их.
Что делать, если вы используете оболочку без массивов? Вы все еще можете использовать позиционные параметры, если не возражаете против их изменения.
Что если вам нужно сохранить сложную команду оболочки, например, с перенаправлениями, каналами и т. Д.? Или если вы не хотите изменять позиционные параметры? Затем вы можете построить строку, содержащую команду, и использовать
eval
встроенную.Обратите внимание на вложенные кавычки в определении
code
: одинарные кавычки'…'
отделяют строковый литерал, так что значением переменнойcode
является строка/path/to/executable --option --message="hello world" -- /path/to/file1
.eval
Встроенный говорит оболочку , чтобы разобрать строку , переданную в качестве аргумента , как если бы он появился в сценарии, так что в этот момент котировка и трубы разобраны и т.д.Использование
eval
сложно. Подумайте внимательно о том, что когда анализируется. В частности, вы не можете просто вставить имя файла в код: вам нужно заключить его в кавычки, как если бы оно было в файле исходного кода. Там нет прямого способа сделать это. Что - то вродеcode="$code $filename"
перерывов , если имя файла содержит какой - либо оболочки специальных символов (пробелы,$
,;
,|
,<
,>
и т.д.).code="$code \"$filename\""
все еще ломается"$\`
. Дажеcode="$code '$filename'"
ломается, если имя файла содержит'
. Есть два решения.Добавьте слой кавычек вокруг имени файла. Самый простой способ сделать это - добавить одинарные кавычки вокруг него и заменить одинарные кавычки на
'\''
.Сохраняйте расширение переменной внутри кода, чтобы оно просматривалось при оценке кода, а не при построении фрагмента кода. Это проще, но работает, только если переменная все еще присутствует с тем же значением во время выполнения кода, а не, например, если код встроен в цикл.
Наконец, вам действительно нужна переменная, содержащая код? Самый естественный способ дать имя блоку кода - это определить функцию.
Что с тобой
read
?Без
-r
,read
позволяет продолжить строки - это одна логическая строка ввода: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
?some_command
должна быть внешней командой, это не может быть функция оболочки или псевдоним. Если вам нужно вызвать оболочку для обработки файлов, вызывайтеsh
явно.У меня есть другой вопрос
Просмотрите тег цитирования на этом сайте, или shell или shell-script . (Нажмите «узнать больше…», чтобы увидеть общие советы и отобранный список общих вопросов.) Если вы искали и не нашли ответа, задайте вопрос .
источник
$(( ... ))
(также$[...]
в некоторых оболочках), за исключениемzsh
(даже в эмуляции sh) иmksh
.xargs -0
это не POSIX. За исключением FreeBSDxargs
, вы обычно хотитеxargs -r0
вместоxargs -0
.ls --quoting-style=shell-always
не совместим сxargs
. Попробуйтеtouch $'a\nb'; ls --quoting-style=shell-always | xargs
xargs -d "\n"
позволяет запускать, например,locate PATTERN1 |xargs -d "\n" grep PATTERN2
поиск имен файлов, соответствующих PATTERN1, и содержимого, соответствующего PATTERN2 . Без GNU вы можете сделать это, например, какlocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
В то время как ответ Жиля превосходен, я беру вопрос в его главном
Когда вы начинаете с Bash-подобной оболочки, которая выполняет разбиение слов, да, конечно, безопасный совет - всегда использовать кавычки. Однако разделение слов не всегда выполняется
§ Разделение слов
Эти команды могут быть выполнены без ошибок
Я не призываю пользователей принять такое поведение, но если кто-то твердо понимает, когда происходит разделение слов, тогда они должны сами решать, когда использовать кавычки.
источник
foo=$bar
это нормально, ноexport foo=$bar
илиenv foo=$var
нет (по крайней мере, в некоторых оболочках). Совет для начинающих: всегда указывайте свои переменные, если вы не знаете, что делаете, и у вас есть веская причина не делать этого .criteria="-type f"
, тоfind . $criteria
работает, ноfind . "$criteria"
не работает.Насколько я знаю, есть только два случая, в которых необходимо заключать в кавычки расширения, и эти случаи включают два специальных параметра оболочки
"$@"
и"$*"
- которые указываются для расширения по-разному, когда заключены в двойные кавычки. Во всех других случаях (за исключением, возможно, реализаций специфичных для оболочки) поведение расширения является настраиваемой вещью - для этого есть варианты.Это, конечно, не означает, что следует избегать двойных кавычек - наоборот, это, вероятно, самый удобный и надежный метод определения границ расширения, которое может предложить оболочка. Но, я думаю, поскольку альтернативы уже были экспертно изложены, это отличное место для обсуждения того, что происходит, когда оболочка расширяет значение.
Оболочка в своем сердце и душе (для тех, у кого она есть) является интерпретатором команд - это синтаксический анализатор, похожий на большой, интерактивный
sed
. Если ваш оператор оболочки задыхается от пробела или аналогичного, то это очень вероятно, потому что вы не полностью поняли процесс интерпретации оболочки - особенно, как и почему она переводит входной оператор в действенную команду. Работа оболочки заключается в следующем:принять ввод
правильно интерпретировать и разбивать его на входные слова
входные слова - это элементы синтаксиса оболочки, такие как
$word
илиecho $words 3 4* 5
слова всегда разделяются на пробелах - это всего лишь синтаксис - но только литеральные пробельные символы, передаваемые оболочке во входном файле
разверните их, если это необходимо, на несколько полей
поля являются результатом расширений слов - они составляют последнюю исполняемую команду
исключая
"$@"
,$IFS
расщепляя поля и раскрывая путь, входное слово всегда должно оцениваться в одном поле .а затем выполнить полученную команду
Люди часто говорят, что оболочка - это клей , и, если это правда, то она придерживается списков аргументов - или полей - тому или иному процессу, когда он
exec
их использует. Большинство оболочек плохо обрабатываютNUL
байт - если вообще - и это потому, что они уже распадаются на него. Оболочка имеетexec
много, и она должна делать это сNUL
массивом аргументов с разделителями, которые она передает ядру системы в тоexec
время. Если бы вы смешали разделитель оболочки с разделенными данными, оболочка, вероятно, испортила бы его. Его внутренние структуры данных - как и большинство программ - полагаются на этот разделитель.zsh
Примечательно, что не облажался.И вот что
$IFS
приходит в голову.$IFS
Это всегда присутствующий - и также устанавливаемый - параметр оболочки, который определяет, как оболочка должна разделять расширения оболочки от слова к полю - в частности, от того, какие значения должны разделять эти поля .$IFS
разбивает расширения оболочки на разделители, отличные отNUL
- или, другими словами, оболочка заменяет байты, полученные в результате расширения, которые соответствуют значениям$IFS
со значениемNUL
в его внутренних массивах данных. Когда вы смотрите на это так, вы можете начать видеть, что каждое расширение оболочки с разделением полей представляет собой$IFS
массив данных с неограниченным доступом.Важно понимать, что
$IFS
только те разграничения, которые еще не разграничены, - это можно сделать с помощью"
двойных кавычек. Когда вы цитируете расширение, вы ограничиваете его в начале и, по крайней мере, до конца его значения. В этих случаях$IFS
не применяется, так как нет полей для разделения. На самом деле, разложение в двойных кавычках демонстрирует поведение разделения поля, аналогичное разложению без кавычек, когдаIFS=
задано пустое значение.Если не указано,
$IFS
само$IFS
расширение оболочки с разделителями. По умолчанию задано значение<space><tab><newline>
- все три из которых проявляют особые свойства, когда содержатся внутри$IFS
. Принимая во внимание, что любое другое значение для$IFS
задано для оценки одного поля в каждом вхождении расширения ,$IFS
пробел - любой из этих трех - определяется для перехода в одно поле в каждой последовательности расширения , а начальные / конечные последовательности полностью исключаются. Это, вероятно, проще всего понять на примере.Но это просто
$IFS
- только расщепление слов или пробел, как их спросили, так что из специальных символов ?Оболочка - по умолчанию - также будет расширять некоторые токены без кавычек (например,
?*[
как указано здесь) в несколько полей, когда они появляются в списке. Это называется расширением пути или глобализацией . Это невероятно полезный инструмент, и, поскольку он происходит после разделения поля в порядке разбора оболочки, на него не влияет $ IFS - поля, сгенерированные расширением пути, ограничиваются заголовком / самим именем файла независимо от того, их содержимое содержит любые символы в настоящее время$IFS
. Это поведение включено по умолчанию, но в противном случае его очень легко настроить.Это указывает оболочка не в Глоб . Расширение пути не произойдет, по крайней мере, до тех пор, пока этот параметр не будет отменен, например, если текущая оболочка заменена другим новым процессом оболочки или ....
... выдан в оболочку. Двойные кавычки - как и для
$IFS
разделения полей - делают этот глобальный параметр ненужным для каждого расширения. Так:... если расширение пути включено в настоящее время, вероятно, будет давать очень разные результаты для каждого аргумента - поскольку первый будет расширяться только до его литерального значения (один символ звездочки, то есть совсем не будет), а второй - только до того же если текущий рабочий каталог не содержит имен файлов, которые могут совпадать (и он соответствует почти всем им) . Однако, если вы делаете:
... результаты обоих аргументов идентичны -
*
в этом случае они не расширяются.источник
IFS
самом деле работает. То , что я не получаю , почему это будет когда - либо быть хорошей идеей , чтобы установитьIFS
нечто иное , чем по умолчанию.$IFS
.cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; done
печатает\n
потомusr\n
потомbin\n
. Первыйecho
пустой, потому что/
это нулевое поле. Компоненты path_components могут иметь символы новой строки или пробелы или что-то еще - не имеет значения, потому что компоненты были разделены,/
а не по умолчанию люди делают этоawk
все время, в любом случае. ваша оболочка тоже это делаетУ меня был большой видео проект с пробелами в именах файлов и пробелами в именах каталогов. Хотя он
find -type f -print0 | xargs -0
работает для нескольких целей и в разных оболочках, я обнаружил, что использование пользовательского IFS (разделителя полей ввода) дает вам большую гибкость, если вы используете bash. Фрагмент ниже использует bash и устанавливает IFS просто как новую строку; при условии, что в ваших именах файлов нет новых строк:Обратите внимание на использование паренов для изоляции переопределения IFS. Я читал другие посты о том, как восстановить IFS, но это просто проще.
Более того, установка IFS на новую строку позволяет заранее установить переменные оболочки и легко их распечатать. Например, я могу постепенно увеличивать переменную V, используя символы новой строки в качестве разделителей:
и соответственно:
Теперь я могу «перечислить» настройку V с
echo "$V"
помощью двойных кавычек для вывода новых строк. (Благодарю эту ветку за$'\n'
объяснение.)источник
zsh
вы используете , вы можете использоватьIFS=$'\0'
и использовать-print0
(zsh
не используйте глобализацию для расширений, поэтому символы глобуса здесь не проблема).set -f
. С другой стороны, ваш подход принципиально не работает с именами файлов, содержащими переводы строк. При работе с данными, отличными от имен файлов, также происходит сбой с пустыми элементами.Принимая во внимание все вышеупомянутые последствия для безопасности и предполагая, что вы доверяете и контролируете переменные, которые вы расширяете, можно использовать несколько путей с использованием пробелов
eval
. Но будь осторожен!источник