Библиотека сайта rus-linux.net
Шаблоны и обработка строк в shell-скриптах
Оригинал: Patterns and string processing in shell scriptsАвтор: Питер Сибах (Peter Seebach)
Дата: 26 декабря 2008 г.
Перевод: Сергей Супрунов
Дата перевода: 02 февраля 2009 г.
Данная статья является отрывком из недавно изданной книги "Beginning Portable Shell Scripting" (Основы разработки переносимых скриптов на языке командной оболочки).
Программирование на языке командной оболочки тесно связано с обработкой строк. Термин "строка" обычно используется для указания любой последовательности символов; типичные примеры строк - вводимые пользователем символы или отдельный аргумент команды. Пользователи вводят запросы в ответ на приглашение командной строки, генерируются имена файлов, команды что-то выводят. Часто при этом бывает нужно определить, соответствует ли заданная строка заданному шаблону; этот процесс называется сопоставлением с шаблоном. Командная оболочка предоставляет довольно развитые средства сопоставления.
К тому же многие стандартные утилиты Unix, такие как grep
и sed
, также предоставляют функции сопоставления с шаблоном. Обычно эти программы используют более мощную разновидность сопоставления, называемую "регулярными выражениями". Регулярные выражения, поскольку они отличаются от шаблонов командной оболочки, играют решающую роль для максимально эффективной работы shell-скриптов. Так как сама оболочка не обладает встроенной поддержкой переносимых (способных одинаково работать в различных средах - прим.перев.) регулярных выражений, программы на shell во многом полагаются на внешние утилиты, многие из которых используют регулярные выражения.
Шаблоны командной оболочки
Шаблоны командной оболочки используются в различных ситуациях. Наиболее обычное их применение - оператор case
. Если задать две переменных, string
и pattern
, то следующий код определит, соответствует ли текст шаблону:
case $string in $pattern) echo "Match" ;; *) echo "No match";; esac
Если $string
соответствует $pattern
, оболочка выведет "Match" и покинет оператор case
. В противном случае она проверит, соответствует ли $string
символу *
. Поскольку *
соответствует абсолютно всему, оболочка напечатает "No match", если не будет найдено соответствие шаблону $pattern
. (Оператор case
исполняет лишь одну ветвь, даже если имеется соответствие нескольким шаблонам.)
Чтобы исследовать сопоставление с шаблонами, может быть полезно создать shell-скрипт наподобие показанного ниже. Следующий скрипт проверят соответствие различных слов некоторому шаблону:
#!/bin/sh pattern="$1" shift echo "Matching against '$pattern':" for string do case $string in $pattern) echo "$string: Match." ;; *) echo "$string: No match." ;; esac done
Сохраните этот скрипт под именем pattern
, сделайте его исполняемым (chmod a+x pattern
), и вы сможете использовать его для выполнения собственных тестов:
$ ./pattern '*' 'hello' Matching against '*': hello: Match. $ ./pattern 'hello*' 'hello' 'hello, there' 'well, hello' Matching against 'hello*': hello: Match. hello, there: Match. well, hello: No match.
Не забывайте заключать аргументы в апострофы. Без них слова, содержащие символы шаблонов, такие как звёздочка (*
), будут подвергаться так называемому расширению имён файлов (globbing), когда оболочка заменяет такие слова именами всех файлов, соответствующих шаблону. Это может привести к неожиданным результатам.
Основы сопоставления с шаблонами
В шаблоне большинство символов соответствуют самим себе, и только себе. Слово "hello" является отличным примером правильного шаблона; он соответствует слову "hello", и ничему больше. Шаблон, соответствующий лишь части строки, не рассматривается как сопоставляемый с этой строкой. Слово "hello" не соответствует тексту "hello, world". Чтобы шаблон соответствовал строке, должны выполняться два условия:
- Каждый символ шаблона должен соответствовать строке.
- Каждый символ строки должен соответствовать шаблону.
Если бы это было всё, что можно сказать о шаблонах, шаблон был бы ещё одним способом описать сравнение строк, и оставшаяся часть этой главы сводилась бы к общим фразам наподобие "... состоит из последовательностей непробельных символов, разделённых пробелами", или, возможно, давала бы несколько великолепных рецептов использования. К сожалению, это не так. На самом деле, есть несколько символов в шаблоне, имеющих специальное значение и способных соответствовать чему-то другому, а не самим себе. Символы, имеющие особое значение в шаблоне, называют символами-заместителями, групповыми символами (wildcards) или метасимволами. Некоторые предпочитают ограничивать использование термина "групповой символ" лишь ссылкой на специальные символы, соответствующие любому набору символов. Говоря о шаблонах, я предпочитаю называть их все групповыми символами, чтобы не путать их с символами, имеющими специальное значение для командной оболочки. (В русскоязычной литературе всё же более распространён термин "метасимвол", его и будем дальше придерживаться - прим.перев.) Метасимволы несколько усложняют приведённые выше простые правила; отдельный символ в шаблоне может соответствовать очень длинной строке, или же группа символов в шаблоне может соответствовать лишь одному символу или даже ни одному. Имеет значение лишь то, что не должно быть несоответствий и ничего не должно оставаться в строке после сопоставления.
Наиболее распространёнными метасимволами являются вопросительный знак (?
), соответствующий любому одному символу, и звёздочка (*
), соответствующая вообще всему, даже пустой строке.
Символ ?
легко использовать в шаблонах; вы ставите его, когда знаете, что здесь будет ровно один символ, но не уверены, какой именно. Например, если вы не знаете точно, с каким "акцентом" пользователь будет приветствовать вас, вы можете использовать шаблон h?llo
, на случай если ваш пользователь предпочитает писать "hallo" или "hullo". Но это оставляет нерешёнными две проблемы. Во-первых, пользователи обычно болтливы, и пишут что-то типа "hello, there" или "hello little computer", а возможно, даже "hello how do i send email". Если вы просто хотите проверить, что получили нечто, хоть немного похожее на приветствие, вам нужен способ описать правило: "данное слово или данное слово плюс что-то ещё в конце".
Вот для этого и нужен символ *
. Поскольку *
сопоставляется со всем, шаблон hello*
будет соответствовать всему, что начинается с "hello", или даже просто строке "hello" без последующих символов. Однако этот шаблон не будет соответствовать строке "well, hello", поскольку в шаблоне нет ничего, что могло бы соответствовать символам перед словом "hello". Обычный приём, когда вы хотите проверить, присутствует ли некоторое слово вообще, заключается в использовании в шаблоне звёздочек с обоих сторон: *hello*
будет охватывать большое число различных приветствий.
Если вы хотите проверить некоторое соответствие, но не уверены в том, что это конкретно будет или какой длины, вы можете совмещать эти метасимволы. Шаблон hello ?*
соответствует фразе "hello world", но не соответствует отдельному слову "hello". Однако этот шаблон порождает новую проблему. Символ пробела не является специальным в шаблоне, но имеет особое значение в командной оболочке. Это приводит к небольшой дилемме. Если вы не заключаете шаблон в кавычки, оболочка разделит его на несколько слов, что не будет соответствовать вашим ожиданиям. Если же использовать кавычки, то оболочка игнорирует метасимволы. У этой проблемы есть два решения: первое - заключать в кавычки пробелы, второе - выносить метасимволы за пределы кавычек. То есть вы можете записать hello" "?*
или "hello "?*
.
В тех случаях, когда оболочка выполняет сопоставление с шаблоном (например, тот же оператор case
), вам не нужно беспокоиться о пробелах, возникающих в результате подстановки переменных; оболочка не будет разбивать переменную в таких ситуациях. (Замечание: zsh
ведёт себя здесь несколько иначе, если она не запущена в режиме эмуляции sh
.)
Классы символов
У шаблона h?llo
есть ещё один недостаток - он слишком многое позволяет. Хотя ваши друзья, которые печатают с ярко выраженным акцентом, без сомнения, будут относиться к вам с уважением, вы вполне можете заполучить строку вроде "hzloo", "h!llo" или "hXllo". Оболочка предлагает механизм более строгого сопоставления, который называется "классы символов". Класс символов соответствует одному символу из набора и больше ничему; он подобен символу ?
, но является более строгим. Класс символов заключается в квадратные скобки ([]
) и выглядит следующим образом: [characters]
. Используя класс символов, описанное выше приветствие можно записать как h[aeu]llo
. Класс символов соответствует в точности одному из включённых в него символов; он никогда не сопоставляется более чем с одним символом.
Классы символов могут включать диапазоны символов. Например, сопоставление с любой цифрой можно задать как [0-9]
. При указании диапазона два символа, разделённые дефисом, рассматриваются как любой символ, попадающий между ними в наборе символов; наиболее часто это используется для букв и цифр. Шаблоны чувствительны к регистру; если вы хотите получить соответствие всем ASCII-буквам, используйте [A-Za-z]
. Поведение диапазона, в котором второй символ следует в наборе символов перед первым, непредсказуемо; не следует так делать.
Если вы не знаете, чего хотите, но точно знаете, чего НЕ хотите, вы можете инвертировать класс символов, используя восклицательный знак (!
) как его первый символ. Класс символов [!0-9]
будет соответствовать любому символу, кроме цифр. Когда класс символов инвертирован, он соответствует абсолютно любому символу, не входящему в диапазон, а не только очевидным или "обычным"; если вы напишете [!aeiou]
, рассчитывая получить согласные буквы, вы также получите символы пунктуации или управляющие символы.
Метасимволы теряют своё специальное значение в классе символов; [?*]
будет соответствовать знаку вопроса или звёздочке, и ничему другому.
Классы символов - один из наиболее сложных аспектов сопоставления с шаблонами в языке командной оболочки. Левая и правая квадратные скобки ([]
), дефисы (-
) и восклицательные знаки (!
) имеют здесь особое значение. Дефис можно легко включить в класс, определив его как самый последний символ класса. Восклицательный знак можно включить как и любой другой символ, только не на первом месте. (А что, если никаких других символов нет? Тогда вы определяете только один символ и, вероятно, класс вам не нужен.) С левой скобкой всё действительно просто; включайте её везде, это не имеет значения. Правая скобка (]
) - это уже особый случай; если вы хотите указать правую скобку, поместите её либо в самом начале списка, либо непосредственно после !
в случае отрицательного класса. В противном случае оболочка будет считать. что правая скобка предназначена для закрытия класса символов. Даже за пределами означенного набора особенностей, имейте в виду, что некоторые оболочки имеют очевидные и простые ошибки, связанные с правыми скобками в классах символов; по возможности, избегайте их.
Если вы хотите задать соответствие любой левой или правой скобке, восклицательному знаку или дефису, но не другим символам, это можно сделать так:
[][!-]
Первая левая скобка начинает определение класса. Первая правая не закрывает класс, поскольку в нём ещё ничего нет; она рассматривается как обычный литерал "правая скобка". Вторая левая скобка и восклицательный знак не имеют специального значения: ни один из этих символов не располагается на позиции, где он мог бы таковое иметь. Наконец, дефис не находится между двумя другими символами в классе, поскольку правая скобка завершает определение класса символов, так что этот дефис должен рассматриваться как обычный символ.
Многие пользователи привыкли использовать символ ^
вместо !
в классах символов командной оболочки. Этот синтаксис не является переносимым, но некоторые оболочки предоставляют его как расширение, поскольку для пользователей, много работающих с регулярными выражениями, это может быть более привычным. Это может привести к неожиданностям, если вы не использовали такой синтаксис раньше и хотите включить символ ^
в класс.
Таблица 2-1 поясняет поведение некоторых символов, которые могут иметь особое значение в классе, а также то, как можно включить их в класс буквально, когда вы этого хотите.
Символ | Значение | Переносимость | Как включить в класс |
---|---|---|---|
] | Конец класса | Универсальный | Поместите его в начало класса (или сразу после символа отрицания) |
[ | Начало класса | Универсальный | Можно размещать в любом месте класса |
^ | Инвертирование | Общепринятый | Размещайте после любого другого символа |
! | Инвертирование | Универсальный | Размещайте после любого другого символа |
- | Диапазон | Универсальный | Размещайте в начале или самом конце класса |
У диапазонов есть ещё одна проблема с переносимостью, которая часто остаётся незамеченной, особенно теми, кто говорит на английском языке. Нет гарантии, что диапазон [a-z]
соответствует любой букве в нижнем регистре, и строго говоря, здесь даже не гарантируется, что он будет соответствовать только буквам в нижнем регистре. Проблема заключается в том, что большинство людей имеют дело с набором символов ASCII, который не определяет символы национальных алфавитов. В ASCII буквы как в верхнем, так и в нижнем регистре располагаются непрерывно (но есть символы между ними; диапазону [A-z]
будет соответствовать и несколько знаков препинания). Однако существуют Unix-подобные системы, где одно или оба этих предположения будут ошибочны. На практике можно достаточно уверенно рассчитывать на то, что диапазону [a-z]
соответствует 26 букв в нижнем регистре. Однако символы национальных алфавитов этому шаблону соответствовать не будут. В общем случае, нет переносимого способа сопоставлять дополнительные символы или хотя бы выяснить, есть ли такие вообще. Скрипты могут запускаться в различных окружениях с различными наборами символов.
Некоторые оболочки поддерживают дополнительную нотацию классов символов; она была введена в POSIX, но до сих пор редко встречается за пределами ksh
(не pdksh
) и bash
. Этой нотацией является [[:class:]]
, где class
- слово наподобие "digit", "alpha" или "punct". Такой класс будет соответствовать любому символу, для которого соответствующая Си-функция isclass()
возвращает истину (true
). Например, [[:digit:]]
является эквивалентом [0-9]
. Эти классы можно комбинировать с другими символами; [[:digit:]][[:alpha:]_]
соответствует любой букве, цифре или символу подчёркивания (_
). Дополнительно используются аналогичные правила - [.name.]
для сопоставления с особыми символами (например, некоторые языки могут иметь особое правило для сопоставления и сортировки определённых комбинаций букв, скажем, "ch" может сортироваться отлично от "c" с последующей "h"), и [=name=]
для сопоставления эквивалентных классов, таких как буква в нижнем регистре и любой её "национальный" вариант. Эти правила особенно полезны для интернационализированных скриптов, но недостаточно широко распространены, чтобы уже сейчас использоваться в переносимых скриптах. Чтобы исключить любые возможные неоднозначности, избегайте использования левой скобки со следующим сразу за ней символом точки (.
), знака равенства (=
) или двоеточия (:
) в классе символов. Заметьте, что это относится только к левой скобке внутри класса символов, а не к первой скобке, открывающей класс. Особый класс [.]
соответствует точке. (Наиболее важно это в регулярных выражениях, где точка в иных случаях имеет специальное значение.)
Классы символов, как вы видите, гораздо сложнее, чем остальные правила сопоставления с шаблонами в командной оболочке.
Шаблоны командной оболочки - довольно мощный инструмент, но он имеет ряд ограничений. Нет способа определить повторение класса символов; никакой шаблон командной оболочки не будет соответствовать произвольному количеству цифр. Вы не можете сделать часть шаблона необязательной; ближе всего к указанию необязательных компонентов можно подойти, используя символ *
.
В целом, шаблоны обычно сопоставляются с как можно большим числом символов; это называется "жадностью". Однако, если сопоставление слишком большого фрагмента символу *
препятствует сопоставлению со всем шаблоном, звёздочка высвобождает дополнительные символы, позволяя другим компонентам шаблона "захватить" их. Если вы сопоставляете шаблон b*
строке "banana", *
будет соответствовать тексту "anana". Однако, если вы используете шаблон b*na
, тогда *
будет сопоставлена лишь с текстом "ana". Правило заключается в том, что *
захватывает столько символов, сколько может, не мешая сопоставлению со всем шаблоном. Другие компоненты шаблона, такие как классы символов, литералы или вопросительные знаки, "захватывают" символы в первую очередь, а звёздочка сопоставляется с тем, что останется.
Некоторые ограничения шаблонов командной оболочки можно преодолеть творчески. Один из способов сохранить список элементов в оболочке - задать множество элементов, соединённых некоторым разделителем; например, вы можете сохранить значение a,b,c
, чтобы представить список из трёх элементов. Следующий пример иллюстрирует, как такой список можно использовать. (Оператор case
, использованный здесь, выполняет код, когда заданная строка соответствует шаблону.)
list=orange,apple,banana case $list in *apple*) echo "How do you like them apples?";; esac How do you like them apples?
Однако этот скрипт содержит одну неочевидную ошибку. Он не проверяет точность сопоставления. Если вы попытаетесь проверить слегка другой список, проблема станет очевидной:
list=orange,crabapple,banana case $list in *apple*) echo "How do you like them apples?";; esac How do you like them apples?
Эта проблема возникает потому, что звёздочка может сопоставляться со всем, даже с запятой, используемой здесь в качестве разделителя. Однако если вы добавите разделители в шаблон, вы больше не будете получать соответствие с элементами в начале и конце списка:
list=orange,apple,banana case $list in *,orange,*) echo "The only fruit for which there is no Cockney slang.";; esac [ничего не выводится]
Чтобы решить эту проблему, "оберните" список дополнительной парой разделителей:
list=orange,apple,banana case ,$list, in *,orange,*) echo "The only fruit for which there is no Cockney slang.";; esac The only fruit for which there is no Cockney slang.
Такое расширение списка $list
добавляет запятые с каждой стороны, что гарантирует, что каждый элемент списка будет окружён запятыми с обеих сторон.