Утилита xMarkup. Руководство пользователя

Версия 2.1.4

Содержание

1. Назначение

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

Данная утилита может успешно использоваться для генерации, редактирования или удаления любых текстовых элементов в исходных файлах. Утилита первоначально разрабатывалась как любительская программа, однако сейчас служит для вполне серьезных вещей - с ее помощью на сайте Русской Виртуальной Библиотеки готовятся все публикуемые тексты. Перечислим очевидные варианты использования утилиты:

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

Утилита реализована в виде консольного приложения для Windows 9x/NT/2K/XP/2003 и написана на языке Icon. С большой долей вероятности утилита должна также работать в Windows Vista.

2. Команда запуска утилиты

Запуск утилиты осуществляется с помощью команды:

xm -fsource [-otarget] [-ppfile] [keys]

Описание опций:

-f source задает пути к исходным файлам, в качестве разделителя используется символ точки с запятой (;).
Пути, заданные со знаком минус (-) впереди исключаются из обработки. Например, если в текущей директории требуется обработать все html-файлы за исключением файла index.html, параметр source должен быть задан как -f*.html;-index.html
Если параметр source задан в виде @file, то список имен обрабатываемых файлов читается из file.
-o target задает путь создания выходных файлов (их имена, как правило, будут совпадать с именами исходных файлов).
"NUL:" задает режим без создания выходных файлов (по умолчанию).
"CON:" задает перенаправление всего вывода на стандартное устройство вывода.
"." задает создание выходных файлов вместе с исходными, но с использованием префикса "xm$" в имени.
Если в качестве target задан путь (с символом-разделителем в конце) к существующей папке, то выходные файлы создаются в ней без сохранения иерархии путей исходных файлов или, если указан ключ , с сохранением.
Если задана произвольная строка, то выходные файлы создаются в текущей папке (т.е. месте запуска утилиты) с добавлением строки target как префикса в их имени. Если указан ключ , то target трактуется как имя директории, которая должна быть создана и использована для вывода с копированием иерархии путей исходных файлов.
-p pfile задает файл правил обработки (по умолчанию xm.par).

Описание ключей:

-r Задает рекурсивный поиск файлов в поддиректориях.
-s Задает предварительную сортировку списка обрабатываемых файлов (по значению путей и имен файлов).
Задает создание выходной папки target, указанной в опции -o, если она не существовала. При сохранении результатов обработки копируется иерархия путей исходных файлов.
-q Подавляет вывод на консоль списка обработанных файлов и итоговой статистики.
-q1 Аналогично опции -q, но подавляется вывод только итоговой статистики.
-d Задает режим отладки.
-x Задает проверку макро-процедур в режиме эмуляции их выполнения (входные файлы при этом не обрабатываются).
-h Печатает краткую справку по правилам обработки.

3. История изменений

Версия 1.0.1, август 1999

Создана первая экспериментальная версия утилиты, которая затем неоднократно переписывалась и улучшалась. Фактически, процесс превращения гадкого утенка в устойчиво работающую программу был завершен только летом 2001 года.

Версия 1.6.3, август 2001

1. Утилита стала максимально расширяемой в смысле возможностей, так как появились процедурные макросы на Icon-диалекте. Т.е. теперь стало возможным писать свои процедуры разметки, в которых доступны вызовы ВСЕХ (!) встроенных функций языка Icon версии 9.3.2.

2. Улучшена работа со счетчиками, число которых теперь теоретически не ограничено! Добавлены специальные макроопределения, позволяющие генерировать значения счетчиков и сбрасывать их в начальное значение. Для совместимости с предыдущими версиями маркапа оставлена возможность автоматической инкрементации счетчиков (параметр autoIncr). Исключено за ненадобностью макроопределение подчиненного счетчика @@counter.

3. Добавлены новые макроопределения проверки позиции в пределах исходного файла (начало или конец файла). В связи с этим из файла параметров исключена секция [Embed], которая позволяла раньше добавлять нужный текст внутрь заголовка html-документа.

4. Введены макроопределения @bol, @eol проверки позиции в пределах строки (начало или конец).

5. Введены новые макроопределения, задающие наборы различных символов и системные переменные, например, текущее время.

6. Добавлено макроопределение @eval, с помощью которого можно вычислить значение выражения "на лету", например, @eval(len(@line)) вернет длину текущей строки.

Версия 1.6.8, февраль 2002

1. Исправлены некоторые мелкие и досадные ошибки.

2. Задание множества обрабатываемых файлов в командной строке списком (@file).

3. Добавлена функция likeword(s), с помощью которой можно проверить, похожа ли подстрока s на слово в составе обрабатываемой строки текста.

4. Добавлены служебные макроопределения @subject (остаток исходной строки, начиная с позиции за стоповым маркером) и @pos (позиция сдвига в @subject).

Версия 1.6.9, июль 2002

Исправлены замеченнные ошибки.

Версия 1.7.0, сентябрь 2004

1. Добавлена функция tabto(i), осуществляющая сдвиг к i-ой позиции подстроки @subject. Позиция сдвига задается макроопределением @pos. Вызов tabto(0) осуществляет сдвиг к концу текущей строки и следовательно задает переход к обработке следующей строки текста.

2. Добавлена функция like(s1,s2) проверяющая строку s1 по поисковой маске s2. Функция возвращает 1 (true), если s1 соответствует маске s2, иначе прерывание &fail.

3. Добавлен отладочный режим, задаваемый параметром debug=true в секции [options].

Версия 1.7.1, февраль 2005

1. Исправлены ошибки при обработке специальных символов (\c) в составе строковых констант.

2. Добавлена операция проверки на неравенство для строковых выражений ( a !== b ).

3. Доработан вывод в режиме отладки.

4. Добавлены ключи "-h" и "-d" в командной строке запуска утилиты.

5. Добавлено макроопределение @nfiles числа обрабатываемых файлов.

6. Переработаны примеры в руководстве в целях их более легкого восприятия.

Версия 1.7.2, март 2005

1. Исправлены ошибки при обработке некоторых макро-определений, заданных в качестве значений стартовых/стоповых маркеров.

Версия 1.7.3, ноябрь 2005

1. Исправлена ошибка при обработке строк, оканчивающихся последовательностью символов обратной косой черты (\).

Версия 1.8.0, сентябрь 2006

1. Добавлены две предопределенные макро-процедуры "initialize" и "finalize", которые автоматически выполняются соответственно в начале и в конце процесса обработки. Данные процедуры при необходимости могут быть заданы в секции [macros].

2. Исправлена ошибка, связанная с обработкой последовательности стартовых маркеров @bof, @bol. В прежних версиях утилиты при обработке такой последовательности маркеров пропускалась первая строка текста.

3. Немного ускорен процесс поиска стартовых/стоповых маркеров.

Версия 2.0, апрель 2007

1. Создан графический интерфейс утилиты.

2. Исправлена ошибка обработки составной многострочной конструкции в фигурных скобках {}, когда закрывающая скобка в отдельной строке вызывала ошибку.

3. Добавлена опция командной строки -c для создания иерархической папки результатов, копирующей иерархию папок исходных файлов.

4. Добавлена опция командной строки -q1 для нужд графического интерфейса.

5. Добавлено макроопределение @regexp() для задания регулярного выражения в качестве стартового/стопового маркера.

6. Доработана функция substr(), которая теперь возвращает пустое значение вместо &fail прерывания, если невозможно выделить заданную подстроку из исходной строки.

7. Исправлена ошибка описания макроопределений @subject и @pos.

8. В состав значений макроопределений @cp866, @cp1251 и @cletters добавлены буквы ё и Ё, ранее пропущенные.

Версия 2.0.1, июль 2007

1. Исправлена ошибка в значении @subject (раньше отсекался последний символ строки).

2. Исключена операция сохранения файла параметров при включенной опции автосохранения, если он на самом деле не изменялся.

Версия 2.1.0, октябрь 2007

1. Добавлена опция -x отладочной проверки макро-процедур в режиме эмуляции их выполнения.

2. Добавлена директива @include, с помощью которой задается включение текста в файл параметров из внешнего файла.

3. Добавлены макро-определения @version, @features, @host, @input, @output, @errout, @e и @pi.

4. Добавлена функция sql_quotes(s), заменяющая одиночный символ апострофа (') на двойной ('') внутри строки s.

5. Доработан графический интерфейс утилиты - добавлена возможность задания оформления консольных и рабочих окон.

6. Создана версия утилиты для POSIX/UNIX-подобных операционных систем.

Версия 2.1.1, апрель 2008

1. Добавлены новые возможности:

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

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

Версия 2.1.2, май 2008

1. Добавлено макроопределение @call для "вызова" из тела макро-процедуры другой макро-процедуры.

2. Исправлена досадная ошибка версии 2.1.1, связанная с неправильным определением пути при чтении/записи файлов, если путь был явно не указан.

Версия 2.1.3, сентябрь 2008

1. Добавлена функция eof() для быстрого перехода в конец исходного файла, чтобы пропустить его обработку.

2. Добавлена функция file_exists(s), которая проверяет существование указанного файла s.

3. Добавлена функция get_separator(), возвращающая символ-разделитель, используемый в пути файла: (/) для Unix или (\) для Windows.

4. Добавлена функция sortfiles(L), которая сортирует список имен файлов L согласно иерархии путей в их именах.

5. Добавлена опция -s, которая задает предварительную сортировку списка обрабатываемых файлов согласно иерархии путей.

6. Отменен режим создания выходных файлов по умолчанию; для создания выходных файлов требуется явно задать путь в опции -o.

7. С сайта http://sourceforge.net/projects/xmarkup/ можно скачать исходный код утилиты, чтобы сделать сборку под необходимую UNIX-платформу (см. главу 9).

Версия 2.1.4, январь 2009

1. Исправлена ошибка в макросе @time, который должен возвращать текущее время выполнения программы.

2. Исправлена ошибка графического интерфейса - в предыдущей версии не работал режим сохранения выходных файлов с добавлением префикса "xm$" к имени исходного файла.

3. На закладке "Помощь" графического интерфейса добавлена ссылка к домашней страничке загрузки программы.

4. Файл правил обработки

Файл правил обработки включает несколько секций, в которых определяются поисковые условия и шаблоны преобразования элементов текста, а также дополнительные параметры обработки. Каждая секция начинается со стандартного заголовка в квадратных скобках []. Имена секций нечувствительны к регистру символов. Состав и порядок секций в файле параметров произвольный. В вырожденном случае файл параметров может включать одну единственную секцию [Macros].

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

4.1 Задание стартовых маркеров

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

Список стартовых маркеров задается в секции [startEntity], например:

[startEntity]
; поиск элементов, начинающихся цифрой
@digits
; поиск HTML-тега title
<title>
; поиск элементов, начинающихся двумя пробелами
@space@space

4.2 Задание стоповых маркеров

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

Список стоповых маркеров задается в секции [stopEntity], например:

[stopEntity]
@sp
</title>
@null

4.3 Задание шаблонов замен

С помощью шаблонов замен задаются процедуры преобразований искомых текстовых элементов. Шаблон замен представляет из себя строку, значение которой подставляется вместо найденного элемента текста (т.е. текста, начинающегося стартовым маркером и заканчивающегося стоповым маркером). В состав шаблона могут включаться как статические элементы, так и динамические, заданные соответствующими макроопределениями.

Список шаблонов замен задается в секции [startMarkup], например:

[startMarkup]
@start<font color="red">@body</font>@stop
@null
@space

Если в каждом случае преобразования требуется выполнить заключительную однотипную подстановку, то она может быть задана в необязательной секции [stopMarkup].

4.4 Задание дополнительных параметров

Дополнительные параметры определяют режимы работы и переменные утилиты. Параметры задаются в необязательной секции [Options]:

Параметр Описание
minBodyLen = i минимальная длина текста между стартовым и стоповым маркерами (по умолчанию 0);
counterInit = i0,i1,... список начальных значений счетчиков (по умолчанию 1);
counterIncr = i0,i1,... список значений приращений счетчиков (по умолчанию 1);
counterType = {REL|ABS} тип счетчиков: REL задает сброс всех счетчиков в начальное значение при открытии очередного обрабатываемого файла;
autoIncr = {true|false} если true, значение счетчика автоматически инкрементируется при каждом вызове макроопределения @counter[];
ignoreCase = {true|false} если true, задается нечувствительный к регистру поиск стартовых и стоповых маркеров;
skipTags = {true|false} если true, при обработке пропускается тело всякого HTML-тега;
syncStop = {true|false} если true, списки стартовых и стоповых маркеров обрабатываются синхронно;
syncMarkup = {true|false} если true, списки стартовых маркеров и шаблонов замен обрабатываются синхронно;
addNewLine = {true|false} если true, в конец каждой строки исходного файла добавляется символ перевода строки.
debug = {true|false} если true, то задается режим отладочной печати, позволяющий проследить процесс обработки текста.

Имена параметров нечувствительны к регистру.

4.5 Задание пропускаемой HTML-разметки

Если требуется пропустить текст внутри разметки, заданной парными открывающим и закрывающим тегами, то имена этих тегов задаются в необязательной секции [tagExceptions]. Например:

[tagExceptions]
head
title
script
table

В этом случае текст, заключенный между открывающим <name> и закрывающим </name> тегами, пропускается при обработке. Однако, пропуск обработки тела самих тегов не гарантируется. В качестве тегов здесь могут использоваться любые элементы с произвольным набором атрибутов.

Если требуется пропустить при обработке тело любых стандартных HTML-тегов, то следует использовать опцию skipTags=true.

4.6 Задание макросов

Процедурные макросы задаются в необязательной секции [Macros]. Использование макросов оправдано в тех случаях, когда требуется произвести нестандартную обработку исходного текста, а не просто "поиск-замену".

Каждая макро-процедура начинается заголовком

macro имя

или

procedure имя

и заканчивается строкой

end

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

Тело макроса состоит из набора предложений макро-языка, описанного в секции 5. Макрос работает как процедура и может возвращать значение, например:
[Macros]
macro increment
  ; если текущая строка начинается числом n, то возращает значение n+1
  if i := many(@digits, @line)-1 then return numeric(substr(@line,1,i)) + 1
end

Макро-процедуры вызываются на выполнение из строки шаблона замен с помощью макроопределения @run(), например

[startmarkup]
@start@run(name)@stop

Все имена функций и переменных внутри макроса являются чувствительными к регистру! Для согласования с правилами Icon для именования переменных рекомендуется использовать только латинские буквы, цифры и символ подчеркивания (причем имя не должно начинаться цифрой).

Типы переменных как и в Icon явно не специфированы, а определяются текущими значениями. Наряду с пользовательскими переменными можно использовать макроопределения утилиты (в режиме read-only) и переменные счетчиков (counter, counterIncr, counterInit). Значения последних задаются как элементы массива и их можно перевычислять, например:

  counter[1] := counterInit[1]
  counter[1] := counter[1] + counterIncr[1]

Начиная с версии 1.8.0 введены две предопределенные макро-процедуры "initialize" и "finalize", которые автоматически выполняются соответственно в начале и в конце процесса обработки списка исходных файлов. С помошью процедуры "initialize" можно инициализировать необходимые переменные, используемых в процессе обработки. С помошью процедуры "finalize" можно выполнить необходимые действия после завершения процесса обработки. Данные процедуры необязательные и могут быть опущены.
[macros]
procedure initialize
  write("Начало обработки...")
  total := 0
end

procedure finalize
  write("Конец обработки.")
  write("total: ",total)
end

5. Описание макро-языка

5.1 Общие сведения

Язык, используемый для задания процедурных макросов, является упрощенным диалектом языка Icon и поддерживает следующие возможности:

  - арифметика целых и вещественных чисел;
  - обработка строковых значений;
  - переменные, макроопределения и массивы;
  - вызовы любых встроенных функций Icon;
  - унарные операторы:
     + (абсолютное значение)
     - (отрицание);
  - оператор присваивания:
     :=
  - бинарные арифметические операторы:
     + (сложение)
     - (вычитание)
     * (умножение)
     / (деление)
     % (вычисление остатка от деления)
     ^ (возведение в степень)
  - оператор конкатенации строк:
     ||
  - логические операторы сравнения для числовых выражений (возвращают 1 при истинности условия
    или 0 в противном случае):
     =  (равенство)
     != (неравенство)
     <  (меньше)
     <= (меньше или равно)
     >  (больше)
     >= (больше или равно)
  - логические операторы сравнения для строковых выражений (возвращают 1 при истинности условия
    или 0 в противном случае):
     ==  (равенство)
     !== (неравенство)
  - блоки предложений в фигурных скобках:
     {}
  - предложения проверки условий:
     if-then
     if-then-else
  - предложения цикла:
     while-do
     every-do
  - строки комментариев, начинающиеся с символа "#" или ";".

Перечисленные возможности были реализованы с помощью программы icalc.icn, разработанной Стивеном Вэмплером (Stephen B. Wampler). Данная программа входит в состав публичной библиотеки программ Icon и предназначена для моделирования работы инфиксного калькулятора. Блестящие идеи, заложенные в этой программе, позволили раширить и доработать ее до уровня интерпретатора Icon-процедур. В какой-то момент пришла мысль использовать этот интерпретатор для xMarkup. Результаты оказались весьма плодотворными.

Для желающих ознакомиться со всеми возможностями языка Icon ныне доступна электронная версия замечательной книги многоуважаемого Ральфа Грисволда The Icon Programming Language.

5.2 Грамматика языка в форме Бэкуса-Науэра

  Предложение ::= Выражение | If | Цикл | Оператор-возврата | Блок-предложений

  Блок-предложений ::= {Список-предложений}

  Список-предложений ::= Предложение | Предложение ; Список-предложений

  If ::= if Выражение then Предложение Else

  Else ::= else Предложение | ""

  Цикл ::= While-цикл | Every-цикл

  While-цикл ::= while Выражение do Предложение

  Every-цикл ::= every Выражение to Выражение do Предложение

  Оператор-возврата ::= return { Выражение | "" }

  Выражение ::= Условие | Переменная := Выражение

  Условие ::= Терм {= | != | < | > | >= | <= | == | !== } Терм | Терм

  Терм ::= T { + | - } Терм | T

  T ::= F { * | / | % } T | F

  F ::= E ^ F | E

  E ::= L | { + | - | || } L

  L ::= Функция | Переменная | Константа | ( Выражение ) | Строка | Множество-символов | Макроопределение | Список

  Функция ::= Идентификатор ( Список-аргументов )

  Переменная ::= Идентификатор | Идентификатор[ Выражение ]

  Константа ::= число (целое или вещественное)
   
  Строка ::= "строка в двойных кавычках"
	
  Множество-символов ::= 'строка в апострофах'

  Макроопределение ::= &Идентификатор | @Идентификатор

  Список ::= [ Список-аргументов ]

  Список-аргументов ::= "" | Выражение | Выражение , Список-аргументов

Каждое предложение макро-языка записывается в отдельной строке и не может быть продолжено на следующих строках. Исключение составляет блок предложений, заключенный в фигурные скобки {}, который может записываться на нескольких строках.

5.3 Переменные

Все переменные, используемые внутри макросов, являются глобальными. Т.е. они сохраняют свои значения при завершении макроса и, кроме того, являются общими для всех объявленных макросов. Типы переменных как и в языке Icon явно не специфированы, а определяются текущими значениями. Имена переменных и функций являются чувствительными к регистру символов.

5.4 Счетчики

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

Количество счетчиков никак не ограничено, а их параметры (начальное значение, тип и приращение) задаются в секции [Options]. Счетчики могут быть двух типов - относительными или абсолютными. Относительные счетчики в отличие от абсолютных автоматически инициализируются при открытии каждого очередного обрабатываемого файла.

Значения i-го счетчика может быть подставлено в шаблоне замены с помощью макроопределения @counter(i) или вызвано в теле макроса с помощью переменной counter[i]. Макроопределение @counter(i) возвращает текущее значение i-го счетчика и, если задан режим автоинкрементации (autoincr=true), автоматически увеличивает его на приращение. При выключенном режиме автоинкрементации (autoincr=false) приращение i-го счетчика может быть задано в шаблоне замен с помощью макроопределения @next(i). Это макропределение не возвращает никакого значения, а только выполняет приращение счетчика. В теле макроса инкрементацию счетчика можно задать с помощью оператора:

counter[i] := counter[i] + counterIncr[i]

Для ручной инициализации i-го счетчика в шаблоне замен может быть использовано макроопределение @reset(i). Это макроопределение не возвращает никакого значения, а только устанавливает счетчик в начальное значение. В теле макроса инициализацию счетчика можно задать с помощью оператора:

counter[i] := counterInit[i]

5.5 Встроенные функции

Ниже дано краткое описание наиболее употребимых функций языка Icon с указанием типа входных параметров и возвращаемых значений. Используемые обозначения:

N - натуральное число; i - целое; r - вещественное; s - строка; c - множество символов; L - список значений (массив); x - любое значение; f - указатель на открытый файл ввода/вывода.

Подробное описание всех функций можно найти на домашней страничке Icon в Аризонском Университете http://www.cs.arizona.edu/icon/.

5.5.1 Числовые функции

abs(N) : N вычисляет абсолютное значение
acos(r1) : r2 вычисляет арк-косинус
asin (r1) : r2 вычисляет арк-синус
atan (r1,r2) : r3 вычисляет арк-тангенс r1/r2
cos(r1) : r2 вычисляет косинус
dtor(r1) : r2 преобразует градусы в радианы
rtod(r1) : r2 преобразует радианы в градусы
exp(r1) : r2 вычисляет экспоненту
iand(i1,i2) : i3 побитовое сложение
icom(i1) : i2 побитовое дополнение
integer(x) : i преобразует x в целое число
ior(i1,i2) : i3 побитовое "включающее-или"
ishift(i1,i2) : i3 сдвиг битов
ixor(i1,i2) : i3 побитовое "исключающее-или"
log(r1,r2) : r3 вычисляет логарифм
numeric(x) : N преобразует значение в число
real(x) : r преобразует x в вещественное значение
sin(r1) : r2 вычисляет синус
sqrt(r1) : r2 вычисляет квадратный корень
tan(r1) : r2 вычисляет тангенс

5.5.2 Строковые функции

any(c,s) : i проверка первого символа строки на принадлежность заданному множеству символов (в случае успеха возвращает 1, иначе fail-прерывание)
сset(s) : с преобразование строки в множество символов
center(s,i) : s2 центрирует строку по ширине i
left(s,i) : s2 сдвигает строку влево по ширине i
left(s1,i,s2) : s3 возвращает строку шириной i символов, в которой s1 позиционируется слева и дополняется справа символами из s2;
например, left("abc",5,"+") вернет "abc++"
right(s,i) : s2 сдвигает строку вправо по ширине i
right(s1,i,s2) : s3 возвращает строку шириной i символов, в которой s1 позиционируется справа и дополняется спереди символами из s2;
например, right("abc",5,"+") вернет "++abc"
ord(s) : i возвращает код символа
char(i) : s возвращает символ по его коду
find(s1,s2) : i поиск подстроки s1 в строке s2 (возвращает позицию в s2, с которой начинается s1, иначе fail-прерывание)
map(s1,c1,c2) : s4 трансляция символов строки s1, принадлежащих множеству символов c1, в соответствующие символы множества c2
many(c,s) : i проверка символов строки на принадлежность заданному множеству символов (возвращает позицию первого символа в s, не входящего в множество c, иначе fail-прерывание)
match(s1,s2) : i проверка совпадения начала строки s2 с подстрокой s1 (в случае успеха возвращает 1, иначе fail-прерывание)
upto(c,s) : i поиск в строке s символов из заданного множества c (в случае успеха возвращает позицию символа в строке s, иначе fail-прерывание)
repl(s,i) : s2 реплицирует строку i раз
reverse(s) : s2 обращает строку задом наперед
string(x) : s преобразует x в строку
trim(s) : s2 обрезает строку от крайних правых пробелов
trim(s, с) : s2 обрезает строку с правого конца от символов, заданных множеством с

Примечание: fail-прерывание вызывает откат выполнения оператора, в котором оно возникло. Это связано с логикой работы языка Icon, который поддерживает backtracking выполняемых операций (аналогично языку Prolog). В случае fail-прерывания текущая операция просто не выполняется. Например, следующий код

...
i := match("123", "qwerty")
write(i)
...

при выполнении вызовет fail-прерывание, в результате чего переменная i останется без значения.

5.5.3 Функции работы со списками

list(i,x) : L создает список длиной i со значениями элементов x
pop(L) : x выталкивает начальное значение из списка
get(L) : x синоним pop()
pull(L) : x выталкивает последнее значение из списка
push(L,x1,x2,...,xn) : L добавляет значения к списку с начала
put(L,x1,x2,...,xn) : L добавляет значения к списку с конца
sort(X) : L сортирует список или множество и возвращает упорядоченный список значений
sort(T,i) : L сортирует таблицу и возвращает упорядоченный список, каждое значение которого есть пара значений (ключ, значение) исходной таблицы. i=1 задает сортировку по значению ключа, 2 - по значению.
sortf(L,i) : L сортирует список по заданному полю i, в том случае если элементы исходного списка также являются списками
sortfiles(L) : L сортирует список имен файлов по значениям пути и имени
set(L) : S создает множество из заданного списка L (множество отличается тем, что в нем все значения уникальные)

5.5.4 Функции ввода/вывода

close(f) : f закрывает файл
getch() : s читает символ с клавиатуры
getche() : s читает символ с клавиатуры с эхом
kbhit() : n проверка нажатия клавиши
open(s,"r") : f открывает файл на чтение
open(s,"w") : f открывает файл на запись
open(s,"a") : f открывает файл на запись в режиме добавления
read(f) : s читает строку из файла
reads(f,i) : s читает файл в буфер заданного размера i
remove(s) : n удаляет файл с заданным именем s
rename(s1,s2) : n переименовывает файл с s1 на s2
seek(f,i) : f позиционирует файл прямого доступа к i-ой позиции
where(f) : i возвращает текущую позицию в файле
write(x1,x2,...,xn) : xn выводит список значений с переводом строки
writes(x1,x2,...,xn) выводит список значений без перевода строки

5.5.5 Системные функции

exit(i) завершает работу утилиты с заданным кодом возврата
chdir(s) : n смена текущей директории на s
getenv(s1) : s2 возвращает значение системной переменной окружения s1
stop(x1,x2,...,xn) прекращает выполнение утилиты и выводит заданный список значений
system(s) : i вызывает системную функцию или программу, заданную именем s
type(x) : s возвращает тип значения

5.5.6 Специальные функции утилиты

len(x) : i возвращает длину строки или списка
substr(s1,i1,i2) : s2 возвращает подстроку длиной i2, начиная с i1-го символа строки s1
lower(s1) : s2 преобразует строку в нижний регистр
upper(s1) : s2 преобразует строку в верхний регистр
sql_quotes(s1) : s2 заменяет каждый одиночный символ апострофа (') внутри строки на двойной ('')
listfiles(s,i) : L возвращает список файлов по заданной маске s; второй параметр - необязательный, если он задан равным 1, то список формируется рекурсивным обходом всех вложенных папок
getpath(s1) : s2 возвращает путь по спецификации файла, например на UNIX getpath("/home/work/file.txt") вернет "/home/work/"
getname(s1) : s2 возвращает имя файла по его спецификации, например на UNIX getname("/home/work/file.txt") вернет "file"
getext(s1) : s2 возвращает тип файла по его спецификации, например на UNIX getext("/home/work/file.txt") вернет "txt"
get_separator() : c возвращает символ-разделитель, используемый в файловых путях
file_exists(s) : i проверяет существование файла, указанного путем s; возвращает 1, если файл существует, иначе 0
get_content(s) : L возвращает список непустых строк заданного файла (аналог макроопределения @read)
close_output() закрывает текущий выходной файл
open_output(s) открывает выходной файл с заданной спецификацией
write_output(x1,...,xn) записывает в выходной файл список значений
isTag(s1) : s2 проверяет, является ли s1 именем HTML-тега в нижнем регистре (возвращает s1 если да, иначе fail-прерывание)
isEsc(s1) : s2 проверяет, является ли s1 именем именованного HTML-символа в нижнем регистре (возвращает этот символ если да, иначе fail-прерывание). Например, isEsc("amp") вернет символ амперсенда (&).
likeword(s) : i проверяет, похожа ли подстрока s в составе текущей строки @subject на слово, т.е. состоит из букв и ограничена символами разделителями (возвращает 1, если да, иначе fail-прерывание)
like(s1,s2) : i проверяет строку s1 по заданной поисковой маске s2 (возвращает 1, если строка соответствует маске, иначе fail-прерывание); в состав маски могут входить символ звездочки (*), задающий любую строку символов и символ подчеркивания (_), задающий один символ
eof() осуществляет быстрый переход в конец обрабатываемого файла и срабатывание маркера @eof (если он задан)
tabto(i) осуществляет сдвиг к i-ой позиции текущей обрабатываемой строки @subject, что позволяет исключить из обработки начальные символы строки до позиции i; сдвиг к концу строки задается i=0; отрицательные значения i задают сдвиг в обратном направлении от конца строки

5.6 Макроопределения

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

@ascii множество 128 символов ASCII-7
@blank список пустых символов [@sp, @tab, "&nbsp;"]
@body подстрока между стартовым и стоповым маркерами
@bof начало исходного файла
@bol начало текущей строки исходного файла
@call s вызывает выполнение макро-процедуры с именем s из тела другой макро-процедуры; это не вызов в чистом виде, а нечто вроде вставки куска кода
@clcase множество 33 строчных русских букв Win-1251
@cletters синоним @cp1251
@clock текущее время в формате "ЧЧ:ММ:СС"
@counter текущее значение 1-го счетчика (оставлено для совместимости с предыдущими версиями)
@counter(i) текущее значение i-го счетчика
@cp1251 множество русских букв в кодировке Win-1251
@cp866 множество русских букв в кодировке DOS-866
@cset множество 256 символов ASCII-8
@cset(s) множество символов, заданных строкой в кавычках
@cucase множество 33 заглавных русских букв Win-1251
@date текущая дата в формате "ГГГГ/ММ/ДД"
@digits множество цифр {0-9}
@eof конец исходного файла
@eol конец текущей строки исходного файла
@e значение числа e = 2,71...
@eval(s) вычисляет и подставляет значение символьного выражения s
@include file вставляет содержимое файла file в тело макро-процедуры
@features список[] характеристик, поддерживаемых текущей реализацией утилиты
@file спецификация текущего обрабатываемого файла
@fileno порядковый номер файла в списке обрабатываемых файлов
@host имя компьютера
@input спецификация текущего обрабатываемого файла (синоним @file)
@letters множество букв латинского алфавита
@lcase множество 26 строчных латинских букв
@line значение текущей строки исходного файла
@lineno порядковый номер текущей строки в файле
@next(i) генерирует следующее значение i-го счетчика
@nfiles общее число обрабатывемых файлов
@nl символ перевода строки
@null пустое значение
@output спецификация текущего выходного файла
@pi значение числа пи = 3,14...
@pos позиция сдвига в подстроке @subject, задаваемая с помощью функции tabto(i)
@q символ двойной кавычки
@read(s) формирует список всех непустых строк файла s (имя файла задается без кавычек)
@regexp(s) поисковый шаблон в виде регулярного выражения, заданного строкой s
@reset(i) сбрасывает i-ый счетчик в начальное значение
@run(s) выполняет процедурный макрос s и подставляет возвращаемое им значение
@semicolon символ точки с запятой (;)
@space символ пробела
@sp символ пробела (синоним @space)
@start значение текущего стартового маркера
@stop значение текущего стопового маркера
@subject остаток текущей обрабатываемой строки, начиная с позиции за стоповым маркером и до конца исходной строки
@tab символ табуляции
@time текущее время выполнения программы в миллисекундах
@ucase множество 26 заглавных латинских букв
@version текущая версия утилиты

Примеры использования макроопределений в составе выражений:

i := match(@space,@line) возвращает 1, если строка начинается пробелом
i := any(cset("abc"),@line) возвращает 1, если строка начинается буквами "a","b" или "c"
i := upto(@ucase,"an ExamplE") возвращает позицию первой заглавной буквы в строке (4)
i := many(@ucase,"") возвращает позицию после начальной последовательности заглавных букв в строке (4)
s := map(@line,"abc","123") преобразует символы "a","b","c" в составе текущей строки на "1","2","3"
( @lineno <= 10 ) истинно для первых 10 строк обрабатываемого файла
like(@line,"Ах*!") истинно для строк, начинающихся на "Ах" и оканчивающихся восклицательным знаком
tabto(0) осуществляет сдвиг к концу текущей строки (пропускаемая часть строки не обрабатывается)

Примечание. Макроопределения, задающие множество значений или позицию (кроме макроопределения @read(s)), не могут использоваться в шаблонах замен. Имена макроопределений как и имена переменных и функций чувствительны к регистру.

5.7 Использование макропределения @null

Макроопределение пустой строки @null может использоваться в трех случаях.

1. Для задания маркеров, начинающихся символами решетки "#", точки с запятой ";" (это символы используются как начало комментариев в файле параметров) или открывающей квадратной скобки "[" (так начинаются секции), например:

[startEntity]
; если маркер начинается символом решетки
@null#01
; если маркер начинается точкой с запятой
@null;01
; eсли маркер начинается открывающей квадратной скобкой
@null[01]
Примечание: для задания произвольных строковых значений в составе маркера можно использовать макро-определение @eval(s). Например, @eval("#01") задаст строку "#01", a @eval("@eval") - строку "@eval".

2. Для задания поиска текста только по одному маркеру - либо стартовому, либо стоповому. Например, поиск подстроки "abc" может быть задан как:

# поиск по стартовому маркеру
[startEntity]
abc

[stopEntity]
@null

или

# поиск по стоповому маркеру
[startEntity]
@null

[stopEntity]
abc

3. В шаблоне замены для задания вырезки найденного текста. Т.е. если указать в качестве шаблона замены @null, то подстрока <старт-маркер><тело><стоп-маркер> окажется вырезанной в результирующем тексте.

5.8 Специальные символы

Специальные символы используются внутри строковых констант (в макросах) и задаются в виде 2-символьных последовательностей, начинающихся символом обратной косой черты (\).

\\ символ обратной косой черты
\" символ двойной кавычки
\q то же, что и \"
\n символ перевода строки (аналог @nl)
\t символ табуляции (аналог @tab)
\r символ возврата каретки
\f символ перехода на новую строку
\xnnn символ, заданный 16-ричным кодом nnn

Примеры.

  s := "\"эта строка будет выведена в кавычках\""
  write_output(s,"\n")

5.9 Использование регулярных выражений

Начиная с версии 2.0 утилиты добавлена возможность использования регулярных выражений при задании стартовых/стоповых маркеров. Данная возможность реализована в результате интегрирования в ядро утилиты процедуры regexp.icn, разработанной Робертом Александером (Robert J. Alexander) и входящей в состав публичной библиотеки программ Icon.

Регулярное выражение задается с помощью макроопределения @regexp(s), например: @regexp("[0-9]*\.[0-9]+"). Смотри главу Примеры, в которой демонстрируется использование регулярных выражений.

В целом формат используемых регулярных выражений очень близок к формату, поддерживаемому UNIX-утилитой "egrep", с учетом расширений для языка Perl. Ниже дается краткое описание специальных символов, используемых в составе регулярных выражений (РВ).

c Любой обычный символ (т.е. ни один из числа, описанных ниже), задающий в РВ сам себя
\c Обратный слэш с последующим символом задает специальный символ (например, \t). Обычно это какой-то непечатный символ
. Точка в составе РВ задает любой одиночный символ; сам символ точки должен быть задан c обратным слэшем впереди (\.)
[строка] Непустая строка символов, заключенная в квадратные скобки, задает совпадение с любым одним символом из указанных в строке. Если первый символ "^", это задает сопадение с любым символом, исключая указанные в строке. Символ "-" между двумя символами задает диапазон последовательных ASCII символов, например, [0-9] эквиавалентно [0123456789]. Другие специальные символы в составе строки задают сами себя.
* Задает сопадение с 0..* экземплярами выражения, указанного слева
+ Задает сопадение с 1..* экземплярами выражения, указанного слева
? Задает сопадение с 0..1 экземплярами выражения, указанного слева
{N} Задает сопадение точно с N экземплярами выражения, указанного слева
{N,} Задает сопадение с минимум N экземплярами выражения, указанного слева
{N,M} Задает сопадение с минимум N но не более M экземплярами выражения, указанного слева
^ Символ крышечки в начале РВ задает совпадение с началом строки
$ Символ доллара в конце РВ задает совпадение с концом строки
| Вертикальная черта задает альтерацию выражений записанных слева и справа
() РВ в круглых скобках задает группу
\N Где N - цифра в диапазоне 1-9, задает повторение N раз выражения в круглых скобках слева. Например, ^(.*)\1$ задает строку, состоящую из двух одинаковых последовательностей символов.

Ниже приводится описание расширенных символов, поддерживаемых языком Perl. Эти символы могут указываться в составе выражений в квадратных скобках [].

\w Задает любой буквенно-цифровой символ (буквы только латинские), включая символ подчеркивания "_"
\W Любой не буквенно-цифровой символ
\b Задает границу слова, заданным как последовательность буквенно-цифровых символов \w
\B Любая не-словесная граница
\s Задает любой символ промежутка
\S Не символ промежутка
\d Любая цифра [0-9]
\D не цифровой символ

5.10 Использование макроопределений @subject и @pos

Макроопределение @subject задает конец текущей обрабатываемой строки, начиная с позиции за стоповым маркером. Макроопределение @pos задает позицию сдвига в @subject (по умолчанию 1). Сдвиг задается с помощью функции tabto(i), где i - позиция сдвига. Сдвиг позволяет пропустить из последующей обработки начало строки @subject до позиции i. Если необходимо исключить из обработки всю строку @subject, то позиция сдвига задается как 0. Если позиция сдвига указывается отрицательной, это означает сдвиг к i-ой позиции, начиная с конца строки. Например, tab(3) задает пропуск первых двух символов строки @subject; tab(-1) задает сдвиг до последнего символа строки; tab(-3) - задает сдвиг до третьего символа с конца. Смотри пример 8.5, в котором показано использование функции сдвига.

5.11 Использование операторов цикла while и every

Использование этих операторов имеет свои нюансы, поэтому имеет смысл рассмотреть их на простых примерах. Пусть мы имеем массив или список значений, которые необходимо каким-либо образом обработать. Для этого нам пригодятся операторы цикла while или every - какой больше нравится.

Оператор while в качестве проверяемого условия может использовать любое выражение, возвращающее числовые значения. Проверка условия дает true, если значение выражения не равно 0, иначе false. Например,

  # распечатаем список значений с помощью оператора цикла while
  array := ["a","b","c"]
  i := 0
  while i < len(array) do write( array[i := i + 1] )

Как это ни странно, но логическое выражение i < len(array) возвращает 1, если условие истинно и 0 в противном случае. Такое уж принято соглашение для логических условий. К слову, выражение while 1 do ... задает выполнение бесконечного цикла.

Другой вариант использования while для обработки списка. В этом случае используется функция get(), которая возвращает очередной элемент списка с одновременным его удалением из списка. После выполнения цикла исходный список станет пустым. Например,

  # распечатаем список значений с помощью оператора цикла while
  array := ["a","b","c"]
  while len(array)>0 do write( get(array) )
  # array после этого станет пустым

При использовании оператора every задается интервал целых значений, определяющий число повторений цикла. Например, every 3 to 5 задает длину цикла равным 3 (т.е. цикл выполняется для значений 3,4,5). В качестве границ интервала можно задавать любые выражения, возвращающие целые значения. Ниже приведен пример задания цикла с помощью оператора every:

  # распечатаем список значений с помощью оператора цикла every
  array := ["a","b","c"]
  i := 0
  every 1 to len(array) do write(array[i := i + 1])

6. Алгоритм обработки текста

Утилита работает подобно конечному автомату. Инструкциями перехода автомата из одного состояния в другое являются заданные поисковые шаблоны и правила преобразования текста. На вход поступает входной текст, на выходе генерируется преобразованный текст. Опишем вкратце алгоритм работы утилиты:

1.Открываем очередной файл из списка обрабатываемых файлов. Если список исчерпан, завершаем работу.
2.Читаем очередную строку файла и встаем в ее начало. Если достигнут конец файла, возвращаемся на шаг 1.
3.Если достигнут конец строки, возвращаемся на шаг 2.
4.Из заданного списка стартовых маркеров выбираем маркер, располагающийся ближе остальных к текущей позиции и имеющий наибольшую длину. Если в текущей строке таких маркеров не существует, возвращаемся на шаг 2.
5.Перемещаемся в позицию за стартовым маркером и выводим текст до маркера "как есть".
6.Если задана опция syncStop=true, то ищем с текущей позиции стоповый маркер, имеющий тот же порядковый номер в списке, что и найденный стартовый маркер. Иначе из заданного списка стоповых маркеров выбираем маркер, встречающийся ближе остальных к текущей позиции.
7.В случае успешного завершения шага 6 в текущей строке переходим на шаг 8. Иначе читаем очередную строку, встаем в ее начальную позицию и возвращаемся на шаг 6.
8.Если задана опция syncMarkup=true, то выбираем шаблон замен, который имеет тот же порядковый номер в списке, что и найденный стартовый маркер. Иначе выбираем первый шаблон из списка. Выводим текст в соответствии с шаблоном замен, сдвигаемся в позицию за стоповым маркером и возвращаемся на шаг 3.

При поиске стартовых/стоповых маркеров важную роль при прочих равных условиях играет их приоритет.
Перечислим маркеры в порядке убывания их приоритета:

  1. @bof
  2. @bol
  3. @null
  4. элемент исходной строки
  5. @eol
  6. @eof

7. Ограничения

Ниже приведен список имеющихся ограничений утилиты:

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

2. Стоповый маркер также должен находиться в пределах одной строки обрабатываемого текста.

3. В определении стартового/стопового маркера можно указывать только единственное макроопределение, задающее множество символов. И более того, ничего другого в такое определение входить не должно. Так, например, значение @digits@letters будет неверным. Это ограничение можно легко обойти, используя регулярные выражения или процедурные макросы.

4. Необходимость задания оператора if ... then ... else ... в одной строке. Это ограничение можно частично снять, используя блоковую конструкцию, например:
  if i = 1 then s := "один" else {
    if i = 2 then s := "два" else {
       if i = 3 then s := "три" else s := ""
       }
    }  
  }

5. Невозможность задания сложных логических условий с использованием логических операторов or, and, not.

6. Значение выражения, заданного в операторе if или while в качестве проверяемого условия, должно быть целого типа (0 = false). Так, например, выражение
while s := read() do write(s)
породит Run-time ошибку, если считанное значение s не число. Вышеприведенный пример было бы правильней записать как
while (s := read()) !== "" do write(s),
что задает циклическое считывание строки из стандартного устройства ввода (консоли) пока она не пустая.

7. Макроопределение @bof может быть задано только для стартового маркера.

8. В силу особенностей реализации утилиты (интерпретатор "в квадрате") ее производительность заведомо ниже производительности исполнимого кода C или C++.

9. Обрабатываемые текстовые файлы должны быть в однобайтовой кодировке ANSI; данные в UTF-8 или Unicode могут быть обработаны не совсем корректно.

8. Примеры использования

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

8.1 Преобразование html-документа в текстовый файл

Смотри пример 8.9, в котором показано как можно сформировать список HTML-тегов.

# html2txt.par
# Вырезка из текста html-разметки.
# Список имен html-тегов задается в файле tags.list
# Пример команды запуска:
# xm -f*.html -phtml2txt.par

[StartEntity]
<!--
<script
<noscript
<@read(tags.list)
</@read(tags.list)

[StopEntity]
-->
</script>
</noscript>
>

[StartMarkup]
@null

[Options]
ignoreCase = true
syncStop   = true

Ниже приведена более универсальная модификация метода, позволяющая удалить любую XML/SGML/HTML разметку.

# conv2txt.par
# Удаление из документа XML/SGML/HTML разметки
[startEntity]
@regexp("<(no)?script")
@regexp("<(/)?(\w)+")
<!doctype
<!--

[stopEntity]
@regexp("</(no)?script>")
>
>
-->

[startMarkup]
@null

[Options]
syncStop = true
ignoreCase = true

8.2 Вырезка из текста пустых строк

# cut_blank_lines1.par
# Вырезка из текста пустых строк, версия 1
# Пример команды запуска:
# xm -f*.txt -pcut_blank_lines1.par
[StartEntity]
@bol

[StopEntity]
@eol

[StartMarkup]
@eval( if len(trim(@line)) > 0 then @line||@nl )

[Options]
addNewLine = false

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

# cut_blank_lines2.par
# Вырезка из текста пустых строк, версия 2
# Пример команды запуска:
# xm -f*.txt -pcut_blank_lines2.par
[StartEntity]
@bol

[StopEntity]
@eol

[StartMarkup]
@run(line)

[Options]
addNewLine = false

[Macros]
procedure initialize
  s := ""
end

procedure line
  if len(trim(@line)) > 0 then { return s||@line; s := @nl }
end

8.3 Вставка в текст колонтитулов

# headers.par
# Вставка в текст верхнего и нижнего колонтитулов
# Пример команды запуска:
# xm -f*.txt -pheaders.par
[StartEntity]
@bof
@eof

[StartMarkup]
<!-- Это верхний колонтитул файла @file -->@nl
<!-- Это нижний колонтитул файла @file -->

[Options]
syncMarkup = true

8.4 Разметка слов в предложениях

Предполагается, что в составе предложения все слова русские.
Предложения заканчиваются точкой, вопросительным или восклицательным знаком.

# words.par
# Разметка слов в предложениях для русскоязычных текстов. 
[startentity]
@cucase
@clcase

[stopentity]
@cset(" \t,-:;\"'.?!")
@eol

[startmarkup]
@run(markup)

[macros]
macro initialize
# sentence_counter - счетчик предложений в тексте
# word_counter - счетчик слов в предложении
# is_end_sentence - признак завершения предложения
  sentence_counter := 0
  word_counter := 0
  is_end_sentence := 1
end

macro markup
  # начало следующего предложения
  if is_end_sentence then {
    sentence_counter := sentence_counter + 1
    word_counter := 0
    write("<sentence id=\"",sentence_counter,"\">")
  }	

  word_counter := word_counter + 1
  write("<word id=\"", sentence_counter, ".", word_counter, "\">",@start,@body,"</word>",@stop)
  # проверим завершение текущего предложения
  if any(".?!", @stop) then { write("</sentence>"); is_end_sentence := 1 } else is_end_sentence := 0
end
Результат обработки текста:
Мама мыла раму. 
Папа пил вино.
Дети играли на улице.
Выглядел бы как
<sentence id="1">
<word id="1.1">Мама</word>
<word id="1.2">мыла</word>
<word id="1.3">раму</word>.
</sentence>
<sentence id="2">
<word id="2.1">Папа</word>
<word id="2.2">пил</word>
<word id="2.3">вино</word>.
</sentence>
<sentence id="3">
<word id="3.1">Дети</word>
<word id="3.2">гуляли</word>
<word id="3.3">на</word>
<word id="3.4">улице</word>.
</sentence>

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

# words2.par
# Разметка слов в предложениях с использованием регулярных выражений
[startentity]
@regexp("[А-Яа-яёЁ]+[\s,-:;\"'\.?!]")

[startmarkup]
@run(markup)

[macros]
macro initialize
  sentence_counter := 0
  word_counter := 0
  is_end_sentence := 1
end

macro markup
# начало следующего предложения
  if is_end_sentence then {
    sentence_counter := sentence_counter + 1
    word_counter := 0
    write("<sentence id=\"",sentence_counter,"\">")
  }	
  word := substr(@start, 1, len(@start)-1)
  word_end := substr(@start, len(@start), 1)
  word_counter := word_counter + 1
  write("<word id=\"", sentence_counter, ".", word_counter, "\">", word, "</word>")
  # проверим конец текущего предложения
  if any(".?!", word_end) then { write("</sentence>"); is_end_sentence := 1 } else is_end_sentence := 0
end

8.5 Удаление из текста избыточных промежутков

В данном примере демонстрируется использование функции сдвига tabto() и макропределения текущей подстроки @subject.

# trim.par
# Удаление из текста избыточных промежутков.
# Последовательности пробелов или символов табуляции в строке 
# сжимаются до одного пробела. Промежутки в конце строки обрезаются.
[startEntity]
@cset(' \t')

[startMarkup]
@run(trim)

[Macros]
procedure trim
# сдвинемся до первого непустого символа в текущей подстроке
  if i := many(' \t', @subject) then tabto(i)
# если мы в конце строки, то исключим конечный пробел
  if @pos >= len(@subject) then return @null
  return @sp  
end

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

# trim2.par
# Удаление из текста излишних промежутков
[StartEntity]
@regexp([\s]+$)
@regexp([\s]+)

[StartMarkup]
@null
@sp

[Options]
syncmarkup=true

8.6 Склейка текстовых файлов

В данном примере множество обрабатываемых текстов объединяется в один выходной файл, имя которого запрашивается в начале (по умолчанию, unite.dat). После завершения обработки печатается количество строк, записанных в выходной файл. В этом примере можно наглядно увидеть преимущества использования макро-процедур initialize и finalize.

# unite.par
# Склейка текстовых файлов в один выходной файл.
[StartEntity]
@eol

[StartMarkup]
@run(line)

[macros]
macro initialize
  writes("Выходной файл [unite.dat]: ")
  if (s:= read()) == "" then s := "unite.dat"
  f := open(s,"w")
  rows := 0
end

macro line
  write(f,@line)
  rows := rows + 1
end

macro finalize
  write(rows, " строк записано в ",s)
end

8.7 Реформатирование длинных строк

В данном примере чрезмерно длинные строки исходного XML/SGML/HTML документа "сворачиваются" в набор более коротких и читабельных строк.

# reformat.par
# Переформатирует строки исходного XML/SGML/HTML документа, превышающие заданную длину.
# Длинные строки "сворачиваются" в набор более коротких и читабельных строк.
# В качестве мест разрыва строки используется начало элементов разметки.
[startEntity]
@bol

[stopEntity]
@eol

[startMarkup]
@run(reformat)

[Options]
addNewLine = false

[Macros]
procedure initialize
# зададим требуемую длину результирующих строк
# (на деле она может быть слегка больше)
  max_len := 80
end

procedure reformat
  line_len := len(@line)
  i := 1
  while (i <= line_len) do { 
     s := substr(@line, i, max_len)
     i := i + max_len
     # нарезаем исходную строку порциями, длиной не менее max_len;
     # в качестве места разрыва используем начало очередного тега
     if (j := upto("<", substr(@line,i))) then { s := s || substr(@line, i, j-1); i := i + j -1 }
     write_output(s, "\n") 
 }
end

8.8 Сортировка текста

Простенький пример сортировки совокупности текстовых строк.

# sort.par
# Сортировка строк множества обрабатываемых текстов. 
# Результирующая совокупность упорядоченных строк выводится на консоль.
# Пример команды запуска:
# xm -f*.txt -psort.par -oNUL:

[startentity]
@bol

[startmarkup]
@run(line)

[macros]
macro initialize
# создадим пустой список
  p := list()
end

macro line
# занесем в список очередную строку
  put(p, @line)
end

macro finalize
# отсортируем список строк и выведем его на консоль
  p := sort(p)
  while len(p) > 0 do write(get(p))
end

8.9 Анализ разметки

В данном примере распечатывается список имен разметочных тегов, использованных в обрабатываемых XML/SGML/HTML документах.

С помощью Icon-функции set(), заданной в процедуре finalize, отсекаются все повторяющиеся имена.

# list_tags.par
# Печать списка имен тегов, использованных в xml/sgml/html документах.
# Пример команды запуска:
# xm -f*.sgm;*.html;*.xml -plist_tags.par -oNUL:
[startentity]
<

[stopentity]
>

[startmarkup]
@run(tag)

[macros]
procedure initialize
  p := list()
end

procedure tag
  t := lower(@body)
  if match("/", t) then return
  if (i := upto(" \t\n",t)) > 0 then t := substr(t,1,i-1)
  put(p, t)
end

procedure finalize
  p := sort(set(p))
  while len(p) > 0 do write(get(p)) 
end

8.10 Подсчет числа строк в файле

# Name: lines-count.par
# Подсчет числа строк в текстовом файле. Печатает текущее время и число строк в файле.
# Пример команды запуска:
# xm -ftext.txt -plines-count.par -onul:
[startentity]
@eof

[startmarkup]
@eval(write(@date||" "||@clock,"\t", @lineno))

8.11 Преобразование XML-данных в формат CSV

Исходные XML-данные (personnel.xml) представлены следующей структурой:
<?xml version="1.0" encoding="windows-1251" ?>
<personnel>
<employee id="7234" mgr="7777"><dept>Sales</dept><name>Tom Scott</name><salary>5700</salary></employee>
<employee id="7777"><name>Alan Cruzo</name><dept>Administration</dept><salary>15000</salary></employee>
<employee id="7001" mgr="1234"><dept>Delivery</dept><name>Jane Fisher</name><salary>6100</salary></employee>
<employee id="1234" mgr="7777"><dept>Delivery</dept><name>John Asher</name><salary>15100</salary></employee>
</personnel>

Необходимо сформировать строки значений следующих атрибутов и элементов:
employee.id, employee.mgr, dept, name, salary

# Name: xml2csv.par
# Преобразование XML-данных в формат CSV (значения через запятую). Результаты выводятся на консоль.
# Пример команды запуска:
# xm -fpersonnel.xml -pxml2csv.par -onul:
[startentity]
<employee
id="
mgr="
<dept>
<name>
<salary>
</employee>

[stopentity]
@null
"
"
</dept>
</name>
</salary>
@null

[startmarkup]
@run(employee)
@run(employee_id)
@run(employee_mgr)
@run(dept)
@run(name)
@run(salary)
@run(write_employee_data)

[options]
syncmarkup=true
syncstop=true

[macros]
macro employee
# Зададим значение атрибута mgr, на случай если он будет опущен
  mgr := ""
end

macro employee_id
# считаем значение атрибута employee.id 
 id := @body
end

macro employee_mgr
# считаем значение атрибута employee.mgr 
 mgr := @body
end

macro dept
# считаем значение элемента dept 
 dept := @body
end

macro name
# считаем значение элемента name 
 name := @body
end

macro salary
# считаем значение элемента salary 
 salary := @body
end

macro write_employee_data
# напечатаем строку значений через заданный разделитель
 d := ","
 write(id, d, mgr, d, dept, d, name, d, salary)
end 

Результаты обработки представленных выше XML-данных будут выглядеть как:
7234,7777,Sales,Tom Scott,5700
7777,,Administration,Alan Cruzo,15000
7001,1234,Delivery,Jane Fisher,6100
1234,7777,Delivery,John Asher,15100

8.12 Анализ частоты слов в тексте

В данном примере реализован простенький алгоритм подсчета частоты слов в тексте. Для выделения слов из текста используется регулярное выражение.
Скорость обработки невысокая, но вполне приемлемая для небольших текстов.

# words-count.par
# Слово задается последовательностью любых латинских и русских букв (в кодировке Windows-1251).
# В составе слова также разрешается символ апострофа или дефиса. 
[startEntity]
@regexp("[A-Za-zА-Яа-яЁё][A-Za-zА-Яа-яЁё\-']*")

[startMarkup]
@run(count)

[Macros]
procedure initialize
# для хранения пар значений (слово,частота) используем таблицу table;
# слово будет ключом в таблице, а частота - его значением (по умолчанию 0)
  t := table(0)
end

procedure count
  word := @start
# проверим текущее слово в таблице:
# если такого слова еще нет, то значением ключа t[word] будет 0
  i := t[word]
  if i = 0 then t[word] := 1 else t[word] := i + 1
end

procedure finalize
# отсортируем таблицу слов по алфавиту (1) или частоте (2)
  orderby := 1
  words := sort(t, orderby)
# каждый элемент структуры words содержит двух-элементный список [слово, частота].
  i := 0
  while len(words) > 0 do { 
    w := get(words)
    i := i + 1 
    write(left(i,5), left(w[1],30), w[2])
  }
end

8.13 Создание SQL-скрипта вставки данных в таблицу из файла в формате CSV

В данном примере из данных в формате CSV формируется SQL-скрипт вставки этих данных в таблицу базы данных. Описание конфигурации (шаблон для предложений insert, количество колонок данных и используемый разделитель) задается во внешнем файле csv2sql.config, загружаемым с помощью директивы @include.
Для контроля символов апострофа (') внтури строки используется функция sql_quotes(s).
Примечание: данный пример будет работать только в версиях утилиты, начиная с 2.1.0.

# csv2sql.par
# Преобразование текста в формате СSV (значения через разделитель)
# в набор SQL-предложений insert into ... values().
[startEntity]
@bol

[stopEntity]

[startMarkup]
@run(markup)

[Options]

[Macros]
procedure initialize
  @include csv2sql.config
  rows := 0
end

procedure markup
  rows := rows + 1
  if rows >= commit_cycle then { write("commit;"); rows := 0 }
  vstr := ""
  s := @line
  j := 0
  while (i := find(delim, s)) do if j >= cols then s:="" else {
    val := "'" || sql_quotes(trim(substr(s,1,i-1))) ||"'"
    s := trim(substr(s, i+1))
    j := j + 1
    if j = 1 then vstr := val else vstr := vstr || ", " || val
  }
  vstr := vstr || ", '" || sql_quotes(s) || "'"
  j := j + 1
  while j < cols do {
    vstr := vstr || ", NULL"
	j := j + 1
  }	
  vstr := vstr || ");"
  write(sql, vstr)
end

# finalize processing
procedure finalize
  write("commit;")
end

Файл конфигурации для данных, сгенерированных в примере 8.11 мог бы выглядеть как:
# csv2sql.config
# Конфигурация для csv2sql.par
# шаблон SQL-предложения:
  sql := "insert into personnel(id, mgr, dept, name, salary) values("
# Символ-разделитель:
  delim := ','
# Число столбцов даннных в исходном CSV-файле:
  cols := 5
# Интервал между commit:
  commit_cycle := 1000

Тогда после обработки данных мы получим следующий SQL-скрипт:
insert into personnel(id, mgr, dept, name, salary) values('7234', '7777', 'Sales', 'Tom Scott', '5700');
insert into personnel(id, mgr, dept, name, salary) values('7777', '', 'Administration', 'Alan Cruzo', '15000');
insert into personnel(id, mgr, dept, name, salary) values('7001', '1234', 'Delivery', 'Jane Fisher', '6100');
insert into personnel(id, mgr, dept, name, salary) values('1234', '7777', 'Delivery', 'John Asher', '15100');
commit;

8.14 Взаимозамена имен в тексте

Пусть в исходном тексте перепутаны имена персонажей и требуется взаимообразно заменить одни имена на другие. Обычным поиском и заменой данную операцию выполнить будет не просто, так как потребуется множество промежуточных шагов. С помощью же xMarkup это можно сделать очень просто и быстро. В следующем тексте (Генезис 11:14)
When Eber had lived 30 years, he became the father of Shelah.
And after he became the father of Shelah, Eber lived 403 years and had other daughters and sons.
необходимо соответственно заменитить "Eber" на "Shelah", "Shelah" на "Eber", "daughters" на "sons" и "sons" на "daughters".

# exchange.par
# Взаимозамена имен в тексте
[startEntity]
Eber
Shelah
daughter
son

[startMarkup]
Shelah
Eber
son
daughter

[Options]
syncMarkup = true

Все, что нам потребовалось сделать - это задать два синхронизированных списка исходных значений и требуемых замен.

8.15 Генерация скрипта обработки исходных файлов

Простой пример, как сгенерить скрипт, который должен последовательно обработать список исходных файлов с помощью некоторой команды. Обратите внимание на вызов функции eof() в составе параметров write(). Функция eof() ничего не возвращает, но осуществляет быстрый переход в конец исходного файла. Для больших файлов это может значительно ускорить процесс.
Примечание: выберите режим обработки без сохранения результатов и вывод без статистики (опции -oNUL: -q консольной утилиты).

# generate-script.par
# Генерация скрипта
[startEntity]
@bof

[startMarkup]
@eval( write("some-command ",@input, eof()) )

Сгенерированный текст скрипта достаточно скопировать с консольного окна и сохранить в BAT-файл.

8.16 Генерация таблицы содержания в html-документе

В данном примере показаны две интересные вещи - переназначение текущего вывода и переформирование "на-лету" его содержимого. Задача следующая: необходимо автоматически сгенерить таблицу содержания для html-документа, содержащего двух-уровневые заголовки <h2> и <h3>. В выходном документе для каждого заголовка необходимо проставить якорный тег, чтобы обеспечить навигацию к нему из таблицы содержания. В общем случае эта задача требует двух проходов - 1) формирования списка заголовков и 2) генерации и вставки таблицы содержания в начало документа. Однако, мы все сделаем за один проход. Стратегия обработки следующая:

Для генерации уникальных идентификаторов якорных тегов используем счетчик counter[1].

# content_table.par
# Генерация таблицы содержания в html-документе.
# Элементами таблицы будут заголовки <h2>...</h2> and <h3>...</h3>.
# SL, 2008/10/15
[startEntity]
@bof
<h2>
<h3>
@eof

[stopEntity]
<body>
</h2>
</h3>
@null

[startMarkup]
@run(header)
@run(level1)
@run(level2)
@run(finish)

[Options]
syncStop = true
syncMarkup = true
counterinit = 0

[Macros]
# инициализация используемых переменных
procedure initialize
  content_table := []
  header := ""
  level := 0
end

# обработка начальной секции (с начала файла до тега <body> включительно)
procedure header
  # запомним начальную часть html-документа
  header:=@body||@stop
  # узнаем путь и имя текущего исходного файла
  file := getpath(@input)||getname(@input)
  # закроем и удалим текущий выходной файл (на этот момент он пустой)
  close_output()
  remove(@output)
  # перенаправим вывод в новый выходной файл
  open_output(file||".tmp")
end

# генерация очередного элемента таблицы содержания
procedure element
  # сгенерируем уникальный ид.элемента (дополненный слева нулями до 3 знаков)
  id := right(counter[1]:=counter[1]+1,3,0)
  # сохраним значение элемента в списке
  put(content_table, "<li><a href=\"#"||id||"\">"||@body||"</a></li>")
  # добавим необходимую разметку (якорный тег) в выходной файл
  write_output("<a name=\"",id,"\"></a>",@start,@body,@stop)
end

# обработка заголовка первого уровня <h2>...</h2>
procedure level1
  if level = 2 then put(content_table,"</ul>")
  level := 1
  @call element
end

# обработка заголовка второго уровня <h3>...</h3>
procedure level2
  if level = 1 then put(content_table,"<ul>")
  level := 2
  @call element
end

# завершение обработки
procedure finish
  # закроем текущий выходной файл и запомним его содержимое
  close_output()
  text := get_content(@output)
  # удалим выходной файл - больше он нам не нужен
  remove(@output)
  # откроем окончательный выходной файл и сформируем его содержимое
  file := file||"_output.html"
  f := open(file,"w")
  # начальная часть...
  write(f,header)
  # таблица содержания...
  write(f,"<h2>СОДЕРЖАНИЕ</h2>")
  write(f,"<ul>")
  while len(content_table)>0 do write(f,get(content_table))
  write(f,"</ul>")
  # тело документа...
  while len(text)>0 do write(f,get(text))
  close(f)
  write(">", file, " создан.")
end

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

В результате использования вышеприведенного скрипта исходный документ преобразуется в следующий выходной документ.

9. Использование утилиты в POSIX/UNIX-подобных операционных системах

Так как Icon является платформенно-независимым языком, то консольную утилиту xmarkup можно использовать на всех платформах, где имеются реализации Icon. В настоящее время доступны реализации Icon для следующих UNIX-платформ:

Для создания бинарного файла утилиты xmarkup на требуемой платформе необходимо установить соответствующий компилятор Icon, а затем собрать утилиту на основе ее исходного кода. Дистрибутивы Icon для различных UNIX-платформ доступны для скачивания на странице Icon-проекта http://www.cs.arizona.edu/icon/v943/. Исходный код утилиты xmarkup доступен для скачивания на http://sourceforge.net/projects/xmarkup/.

9.1 Установка компилятора Icon с помощью бинарного дистрибутива

9.2 Сборка и установка компилятора Icon из исходного кода

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

9.3 Сборка утилиты xmarkup

       #проверьте, чтобы в файле присутствовала строка 
       $define _UNIX 1
       #задайте имя вашей системы (чтобы уточнить это имя воспользуйтесь командой "uname -a")
       $define ENV "FreeBSD"
       IPATH=/usr/bin/icon.v943/bin

© Сергей Логичев, 1999-2008