Наши партнеры

UnixForum






Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Тематический каталог -> Работа в консоли Linux

Шаблоны и обработка строк в 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 поясняет поведение некоторых символов, которые могут иметь особое значение в классе, а также то, как можно включить их в класс буквально, когда вы этого хотите.

Таблица 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 добавляет запятые с каждой стороны, что гарантирует, что каждый элемент списка будет окружён запятыми с обеих сторон.