Почему мой сценарий оболочки задыхается от пробелов или других специальных символов?

Или вводное руководство для надежной обработки имен файлов и других строк, передаваемых в сценариях оболочки.

Я написал сценарий оболочки, который работает хорошо в большинстве случаев. Но он задыхается на некоторых входах (например, на некоторых именах файлов).

У меня возникла такая проблема, как:

  • У меня есть имя файла, содержащее пробел hello world, и оно рассматривалось как два отдельных файла hello и world.
  • >
  • У меня есть строка ввода с двумя соседними пробелами, и они сокращаются до единицы на входе.
  • Ведущие и конечные пробелы исчезают из строк ввода.
  • Иногда, когда вход содержит один из символов \ [*?, они заменен на некоторый текст, который на самом деле является именем файлов.
  • На вкладке есть апостроф ' (или двойная кавычка "), и после этой вещи все стало странно.
  • На входе есть обратная косая черта (или: я использую Cygwin, а некоторые из моих имен файлов имеют разделители в стиле \ в стиле Windows).

Что происходит и как его исправить?

224 голоса | спросил Gilles 24 Mayam14 2014, 07:25:05

4 ответа


280

Всегда используйте двойные кавычки вокруг подстановок переменных и подстановок команд: "$ foo", "$ (foo)"

Если вы используете $ foo без кавычек, ваш скрипт будет подавлять вход или параметры (или вывод команды с кодом $ (foo)), содержащий пробельные символы или [*?.

Там вы можете прекратить чтение. Ну, ладно, вот еще несколько:

  • read â € " Чтобы прочитать ввод по строкам с помощью встроенного read, используйте , а IFS = read -r line; do â € |
    Обычный read обрабатывает обратную косую черту и пробелы.
  • xargs â € " Избегайте xargs . Если вы должны использовать xargs, сделайте это xargs -0. Вместо find â | | | xargs, предпочитают find â € |Â -exec â € | .
    xargs обрабатывает пробелы и символы \ "'.

Этот ответ применяется к оболочкам Bourne /POSIX (sh, ash, dash, bash), ksh, mksh, yash â € |). Пользователи Zsh должны пропустить его и прочитать конец Когда требуется двойное цитирование ? . Если вы хотите, чтобы весь nitty-gritty, прочитал стандарт или руководство вашей раковины.


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

Зачем мне писать "$ foo"? Что происходит без кавычек?

$ foo не означает «получить значение переменной foo». Это означает нечто гораздо более сложное:

  • Сначала возьмите значение переменной.
  • Разделение полей: рассматривайте это значение как список полей, разделенных пробелами, и создайте результирующий список. Например, если переменная содержит foo * bar â € <, то результатом этого шага будет 3-элементный список foo, *, бар.
  • Генерация имени файла: обрабатывайте каждое поле как glob, т. е. как шаблон подстановочного знака, и заменяйте его на список имен файлов, соответствующих этому шаблону. Если шаблон не соответствует никаким файлам, он остается неизмененным. В нашем примере это приводит к списку, содержащему foo, следуя списку файлов в текущем каталоге и, наконец, bar. Если текущий каталог пуст, результатом является foo, *, bar.

Обратите внимание, что результатом является список строк. В контексте синтаксиса оболочки есть два контекста: контекст списка и контекст строки. Разделение поля и генерация имени файла происходят только в контексте списка, но это в большинстве случаев. Двойные кавычки ограничивают контекст строки: вся строка с двумя кавычками - это одна строка, которую нельзя разделить. (Исключение: "$ @", чтобы перейти к списку позиционных параметров, например "$ @" эквивалентно "$ 1" "$ 2" "$ 3", если есть три позиционных параметра. См. Что такое разница между $ * и $ @? )

То же самое происходит с подстановкой команд с помощью $ (foo) или с помощью `foo`. На стороне заметьте, не используйте `foo`: его правила цитирования являются странными и не переносимыми, а все современные оболочки поддерживают $ (foo), что абсолютно эквивалентно за исключением наличия интуитивных правил цитирования.

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

См. Когда требуется двойное цитирование? для получения более подробной информации о случаи, когда вы можете оставить цитаты.

Если вы не хотите, чтобы все это произошло, просто помните, что всегда используйте двойные кавычки вокруг переменных и подстановок команд. Позаботьтесь о том, чтобы оставить кавычки, может привести не только к ошибкам, но и к дыры в безопасности .

Как обрабатывать список имен файлов?

Если вы пишете myfiles = "file1 file2", с пробелами для разделения файлов, это не может работать с именами файлов, содержащими пробелы. Имена файлов Unix могут содержать любой символ, отличный от / (который всегда является разделителем каталогов) и нулевыми байтами (которые вы не можете использовать в сценариях оболочки с большинством оболочек).

Такая же проблема с myfiles = *. txt;• обработать $ myfiles. Когда вы это делаете, переменная myfiles содержит 5-символьную строку *. Txt, и это когда вы пишете $ myfiles, что шаблон расширен. Этот пример будет работать, пока вы не измените свой скрипт на myfiles = "$ someprefix * .txt"; • обработать $ myfiles. Если someprefix установлен в окончательный отчет, это не сработает.

Чтобы обработать список любого типа (например, имена файлов), поместите его в массив. Для этого требуются mksh, ksh93, yash или bash (или zsh, которые не имеют всех этих проблем цитирования); простая оболочка POSIX (такая как зола или тире) не имеет переменных массива.

MyFiles = ( "$ someprefix" *. TXT)
process "$ {myfiles [@]}"

Ksh88 имеет переменные массива с другим синтаксисом присваивания set -A myfiles "someprefix" *. txt (см. в другой среде ksh , если вам нужна переносимость ksh88 /bash). В оболочках Bourne /POSIX есть один массив, массив позиционных параметров "$ @", который вы устанавливаете с помощью set и который является локальным для функции:

set - "$ someprefix" *. txt
процесс - "$ @"

Как насчет имен файлов, начинающихся с -?

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

В качестве альтернативы вы можете убедиться, что имена файлов начинаются с символа, отличного от -. Абсолютные имена файлов начинаются с /, и вы можете добавить ./ в начале относительных имен. Следующий фрагмент превращает содержимое переменной 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 "интересный материал"
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'" breaks, если имя файла содержит '. Существует два решения.

  • Добавьте слой котировок вокруг имени файла. Самый простой способ сделать это - добавить одинарные кавычки вокруг него и заменить одинарные кавычки на '\' '.

    quoted_filename = $ (printf% s. "$ filename" | sed "s /'/' \\\\ '' /g")
    code = "$ code '$ {quoted_filename%.}'"
    
  • Сохраняйте расширение переменной внутри кода, чтобы он просматривался, когда код оценивался, а не когда был создан фрагмент кода. Это проще, но работает только в том случае, если переменная по-прежнему находится на одном и том же значении во время выполнения кода, а не напр. если код построен в цикле.

    code = "$ code \" \ $ filename \ ""
    

Наконец, вам действительно нужна переменная, содержащая код? Наиболее естественным способом дать имя блоку кода является определение функции.

Что происходит с read?

Без -r, read позволяет строки продолжения - это одна логическая строка ввода:

hello \
Мир

read разбивает строку ввода на поля, помеченные символами в $ IFS (без -r, обратная косая черта также ускользает от них). Например, если вход представляет собой строку, содержащую три слова, тогда читать первую третью третью цифру устанавливает first в первое слово ввода, second, чтобы второе слово и третье третье слово. Если есть больше слов, последняя переменная содержит все, что осталось после установки предыдущих. Верхние и конечные пробелы обрезаны.

Установка IFS в пустую строку позволяет избежать любой обрезки. См. Почему «IFS = read» используется так часто, а не `IFS =; в то время как read..`? для более подробного объяснения.

Что случилось с 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 явно.

найти â € | -exec sh -c '
  для x do
    â € | # обработать файл «$ x»
  сделанный
'find-sh {} +

У меня есть другой вопрос

Просмотрите на этом сайте, или или . (Нажмите «Найти больше», чтобы увидеть некоторые общие советы и список выбранных вручную вопросов). Если вы искали и не можете найти ответ, откройте .

ответил Gilles 24 Mayam14 2014, 07:25:05
20

В то время как ответ Гилла превосходный, я беру вопрос в его основной точке

  

Всегда используйте двойные кавычки вокруг переменных подстановок и команды   подстановки: «$ foo», «$ (foo)»

Когда вы начинаете с Bash-подобной оболочки, которая разделяет слово, да Конечно, безопасный совет всегда используется для котировок. Однако расщепление слов не всегда выполняется

§ Разделение слов

Эти команды можно запускать без ошибок

Foo = $ бар
bar = $ (команда)
logfile = $ logdir /foo - $ (дата +% Y% m% d)
PATH = /usr /local /bin: $ PATH ./myscript
case $ foo в баре) echo bar ;; baz) echo baz ;; ESAC

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

ответил Steven Penny 24 Maypm14 2014, 12:05:48
17

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

Это не означает, конечно, что следует избегать двойного цитирования - напротив, это, вероятно, самый удобный и надежный способ разграничения расширения, которое может предложить оболочка. Но, я думаю, по мере того, как альтернативы уже были тщательно изложены, это отличное место для обсуждения того, что происходит, когда оболочка расширяет значение.

Оболочка, в ее сердце и душе (для тех, кто имеет такие) , является командным интерпретатором - это синтаксический анализатор, как большой интерактивный, sed . Если ваш оператор оболочки choking в whitespace или аналогичный, то это очень вероятно, потому что вы не полностью поняли процесс интерпретации оболочки - особенно, как и почему он преобразует входной оператор в действующая команда. Задача оболочки:

  1. принять ввод

  2. интерпретировать и расколоть правильно на токенизированные входные слова

    • ввод слова - это элементы синтаксиса оболочки, такие как $ word или echo $ words 3 4 * 5

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

  3. разложите их, если необходимо, в несколько полей

    • поля результат из word расширений - они составляют окончательную исполняемую команду

    • кроме "$ @", $ IFS разбиение полей и расширение имени пути ввод word должен всегда оцениваться в одном поле .

  4. , а затем выполнить результирующую команду

    • в большинстве случаев это включает в себя передачу результатов его интерпретации в той или иной форме

Люди часто говорят, что оболочка является клеем , и, если это так, то что это sticking - это списки аргументов - или поля em> - в тот или иной процесс, когда он exec с ними. Большинство оболочек плохо обрабатывают байт NUL - если вообще - и это потому, что они уже раскалываются на нем. Оболочка должна exec много , и она должна сделать это с помощью массива аргументов NUL с разделителями, который передается системному ядру в exec. Если бы вы смешивали разделитель оболочки с разделительными данными, то оболочка, вероятно, испортила бы ее. Его внутренние структуры данных - как и большинство программ - полагаются на этот разделитель. zsh, в частности, не испортить это.

И вот что входит $ IFS. $ IFS - это всегда присутствующий - и аналогичный параметр устанавливаемого - shell, который определяет, как оболочка должна разделять расширения оболочки от word в поле - конкретно, какие значения должны использовать те поля . $ IFS разделяет расширения оболочки на разделителях, отличных от NUL - или, другими словами, оболочка заменяет байты, полученные в результате расширения, которые соответствуют значениям в значении $ IFS с NUL в своих внутренних массивах данных. Когда вы посмотрите на это, вы можете увидеть, что каждое расширение оболочки field-split представляет собой массив данных $ IFS -delimited.

Важно понимать, что $ IFS только ограничивает расширения, которые not уже разграничены, что вы можете сделать с помощью ". При цитировании расширения вы ограничиваете его по голове и как минимум до хвоста его значения. В этих случаях $ IFS не работает применяются, поскольку нет полей для разделения. Фактически, двойное кавычное расширение демонстрирует идентичное поведение разбиения полей на на некотируемое расширение, когда IFS = устанавливается на пустое значение .

Без кавычек $ IFS сам является расширением оболочки с расширением $ IFS. Он по умолчанию задает значение <space> <tab> <newline> - все три из которых обладают специальными свойствами, если они содержатся в $ IFS. В то время как любое другое значение для $ IFS указано для оценки в одном поле для каждого расширения , $ IFS < em> whitespace - любой из этих трех - указывается для исключения одного поля на разводку sequence , и ведущие /конечные последовательности полностью исключаются. Это, вероятно, проще всего понять на примере.

slashes = /////spaces = ''
КСФ = /; printf '<% s>'$ Слэши $ пространства
& Lt; > & Lt; > & Lt; > & Lt; > & Lt; > & Lt; >
IFS = ''; printf '<% s>' $ Слэши $ пространства
& Lt; /////>
IFS =; printf '<% s>' $ Слэши $ пространства
</////>
unset IFS; printf '<% s>' «$ Слэши $ пространства»
</////>

Но это просто $ IFS - просто слово-расщепление или whitespace , как было задано, так что из специальных символов ?

Оболочка - по умолчанию - также расширит определенные некотируемые токены (такие как ? * [, как указано в другом месте здесь) в несколько полей когда они встречаются в списке. Это называется расширением имени пути или globbing . Это невероятно полезный инструмент, и, как это происходит после разделения полей в синтаксическом разборе оболочки, на него не влияют поля $ IFS - , сгенерированные расширением pathname, ограничены заголовком /хвостом самих имен файлов, независимо от того, содержат ли их содержимое какие-либо символы в настоящее время в $ IFS. По умолчанию этот режим включен, но в противном случае его легко настроить.

set -f

Это указывает оболочке not на glob . Расширение имени пути не произойдет, по крайней мере, до тех пор, пока эта настройка не будет отменена - например, если текущая оболочка заменена другим новым процессом оболочки или ...

set + f

... выдается оболочке. Двойные кавычки - так же, как и для $ IFS разделения полей - делают этот глобальный параметр ненужным для расширения. Итак:

echo "*" *

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

set -f; echo "*" *

... результаты для обоих аргументов идентичны - * не расширяется в этом случае.

ответил mikeserv 14 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowSun, 14 Sep 2014 22:41:50 +0400 2014, 22:41:50
0

У меня был большой видеопроект с пробелами в именах файлов и пробелах в именах каталогов. Пока find -type f -print0 | xargs -0 работает в нескольких целях и в разных оболочках, я нахожу, что использование пользовательского IFS (разделителя полей ввода) дает вам большую гибкость, если вы используете bash. В приведенном ниже фрагменте используется bash и задается IFS только для новой строки; если в ваших именах нет новых строк:

(IFS = $ '\ n'; для i в $ (find -type f -print), do
    echo ">> $ i <<<
сделанный)

Обратите внимание на использование parens для изоляции переопределения IFS. Я читал другие сообщения о том, как восстановить IFS, но это проще.

Далее, установка IFS в новую строку позволяет заранее установить переменные оболочки и легко распечатать их. Например, я могу вырастить переменную V поэтапно, используя символы новой строки как разделители:

V = ""
V = "./Ralphie's Camcorder /STREAM /00123.MTS, 04: 58,05: 52, -vf yadif"
V = "$ V" $ '\ n' "./Ralphie's Camcorder /STREAM /00111.MTS, 00: 00,59: 59, -vf yadif"
V = "$ V" $ '\ n' "следующий пункт идет здесь ..."

и соответственно:

(IFS = $ '\ n'; для v в $ V; do
    echo ">> $ v <<
сделанный)

Теперь я могу «перечислить» настройку V с помощью echo «$ V», используя двойные кавычки для вывода новых строк. (Подтвердите этот поток для объяснения $ '\ n'.)

ответил Russ 28 FebruaryEurope/MoscowbWed, 28 Feb 2018 16:25:55 +0300000000pmWed, 28 Feb 2018 16:25:55 +030018 2018, 16:25:55

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

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132