Содержание


Guile — универсальный инструмент программирования. Часть 2. Как с ним обращаться

Comments

Серия контента:

Этот контент является частью # из серии # статей:

Следите за выходом новых статей этой серии.

Этот контент является частью серии:

Следите за выходом новых статей этой серии.

Интерпретатор лучше всего оценивать с точки зрения его практического применения. Разнообразные варианты командных оболочек shell, Perl, Python уже стали повседневными инструментами опытных пользователей, и на общем фоне Guile выглядит несколько непривычно. Тем не менее, этот интерпретатор способен выполнять весьма полезную работу, о чём и пойдёт речь далее.

1. Интерпретатор. Примеры скриптов

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

1.1. Поиск в файле по заданному образцу

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

#!/usr/bin/guile -s
!#
(use-modules (ice-9 rdelim))
(define arg (cdr (command-line)))
(define file (car arg))
(define pattern (cadr arg))
(display (string-append "Файл: " file))
(newline)
(display (string-append "Образец поиска: " pattern))
(newline)
(display "***** Найдено:")
(newline)
(let ((p (open-file file "r")))
  (let loop ((line (read-line p)))
    (cond ((not (eof-object? line))
      (if (string-contains line pattern)
        ((lambda (s) (display s) (newline)) line))
      (loop (read-line p)))))
      (close p))

Как известно, скрипты на интерпретируемых языках в первой строке исходного кода должны самоидентифицироваться в соответствии со стандартом POSIX для того, чтобы определить среду выполнения. В случае Guile самая первая строка несёт двойную смысловую нагрузку. С точки зрения системы комбинация символов #! обозначает строку идентификации интерпретатора, который будет выполнять данный скрипт. С точки зрения Guile сочетание символов #! сообщает о том, что здесь начинается многострочный комментарий, а завершаться он должен комбинацией символов !#. Такое решение и системные требования позволяет выполнить, и исключить из рассмотрения не нужные интерпретатору строки.

В третьей строке (после идентификации скрипта) показано, каким образом можно подключать необходимые модули. В данном случае предписывается использование модуля (ice-9 rdelim), который предоставляет удобную функцию чтения строк из входного потока read-line, а также много других полезных функций. Вообще говоря, в дистрибутивный комплект Guile включено большое количество модулей. Для того, чтобы описать их даже кратко, потребуются не две и не три статьи, а небольшая книжка. Впрочем, в документации — в "Справочном руководстве Guile" такие описания есть для всех модулей.

В строках 4-6 выполняется обработка аргументов командной строки. При запуске скрипта интерпретатор Guile делает аргументы командной строки доступными при помощи процедуры (command-line), которая возвращает передаваемые аргументы в виде списка строк. Но здесь необходимо помнить о том, что в качестве самого первого аргумента передаётся имя файла скрипта. Если предположить, что приведённый выше скрипт сохранён в файле с именем guile02-01, то список аргументов, возвращаемый процедурой (command-line), будет выглядеть приблизительно так:

("./guile02-01" "имя_файла_поиска" "образец_поиска")

Поэтому для удобства работы с аргументами мы отбросим ненужное в данном случае имя файла скрипта и сохраним остальные элементы списка в специально созданной здесь же переменной arg. Функция cdr позволяет получить так называемый "хвост" списка, то есть, список, содержащий все элементы, кроме первого. Именно это нам и нужно. Итак, в четвёртой строке скрипта определяется (define) новая переменная arg, с которой связывается список из двух элементов-строк ("имя_файла_поиска" "образец_поиска").

В пятой строке определяется переменная file, которая связывается со строкой, представляющей имя файла, назначаемого для поиска. Получить эту строку из списка аргументов позволяет функция car — она возвращает "голову" списка, а именно, самый первый его элемент.

Ещё одна переменная pattern создаётся в шестой строке; она предназначена для хранения строки образца поиска. Чтобы извлечь эту строку из списка аргументов, пришлось воспользоваться двумя операциями (car (cdr arg)), которые можно записать в сокращённой форме (cadr arg). При извлечении элементов из середины длинных списков может потребоваться достаточно много комбинаций car и cdr, и в этих случаях сокращённая форма записи (cadaddaadddr (очень ... длинный ... список)) помогает избавиться от многочисленных скобок. Наш случай достаточно простой, но тем не менее, и здесь возникает вопрос: почему нельзя было ограничиться одной операцией (cdr arg)? Ведь в "хвосте" списка аргументов остаётся только один элемент — строка образца поиска. Дело в том, что cdr всегда возвращает список, даже если он состоит из одного элемента, и мы получим список ("образец"), а не строку "образец". Чтобы извлечь из этого списка требуемую строку, как раз необходима функция car, возвращающая первый элемент списка (но не список!). В результате мы получаем то, что нужно — строку-образец.

В строках 7-12 выполняется вывод предварительной, "заголовочной" информации. Для вывода я использовал функцию display, потому что она обеспечивает более удобочитаемое представление выводимой информации в отличие, например, от функции write. Функция string-append позволяет объединять несколько строк, а (newline) выполняет переход на новую строку.

Основная работа выполняется в строках с 13 по 19. Конструкция let предназначена для организации локальных связей, то есть, фактически для обеспечения блочной структуры программы. Форму let можно считать синтаксическим видоизменением lambda-функции с той лишь разницей, что в let формальные и фактические параметры размещены в начале формы совместно, в виде пар (переменная выражение). В нашем случае (строка 13) только одна переменная p связывается с портом, представляющим открываемый файл с именем, задаваемым содержимым переменной file, в режиме только для чтения ("r").

В строке 14 во вложенном блоке let организовывается циклическое считывание строк из файлового порта p. На каждой итерации производится проверка на наличие признака конца файла (строка 15), если считана очередная строка, то в ней выполняется поиск заданного образца (строка 16). Если образец содержится в текущей строке, то эта строка выводится на экран (строка 17). Цикл завершается при обнаружении признака конца файла. После этого файловый порт p закрывается, и скрипт заканчивает работу.

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

первая строка first line
вторая строка second line
третья строка third line
четвёртая строка fourth line
пятая строка fifth line
предпоследняя строка before last line
последняя строка last line

Дадим этому файлу имя words.txt и с его помощью протестируем работу скрипта. Результаты показаны на рис.2.1.

Рис.2.1. Результаты поиска в файле
Рис.2.1. Результаты поиска в файле
Рис.2.1. Результаты поиска в файле

1.2. Обработка всех файлов в заданном каталоге

Ещё один небольшой пример, в котором показано, как Guile может работать с файлами и каталогами.

#!/usr/bin/guile -s
!#
(define arg (cadr (command-line)))
(define dir (opendir arg))
(define pathfile ")
(do ((file (readdir dir) (readdir dir)))
  ((eof-object? file))
  (set! pathfile (string-append arg "/" file))
  (display (string-append pathfile " — "))
  (case (stat:type (stat pathfile))
    ((regular) (display "обычный файл"))
    ((directory) (display "каталог"))
    ((symlink) (display "ссылка"))
    ((block-special) (display "блоковое устройство"))
    ((char-special) (display "символьное устройство"))
    ((fifo) (display "канал"))
    ((socket) (display "сокет"))
    ((unknown) (display "неизвестный тип")))
  (newline))
(closedir dir)

При обработке аргументов командной строки в переменной arg сохраняется имя заданного каталога. Далее с помощью функции opendir открывается заданный каталог, а указатель на него (на его "поток") связывается с переменной dir. Кроме того, определяется вспомогательная переменная pathdir, которая потребуется нам для формирования путевого имени.

В этом скрипте наиболее общая циклическая (итерационная) конструкция do. В ней file является внутренней переменной, для которой начальным значением является результат считывания записи из каталога — первое выражение (readdir dir). Второе выражение (readdir dir) представляет "шаг" цикла, то есть, на каждой новой итерации будет считываться очередная запись из каталога. После этого записывается условие окончания цикла, в нашем случае — признак конца файла (eof-object? file). Далее следуют выражения тела цикла: изменение значения переменной pathdir с помощью команды set!, вывод имени текущего элемента, конструкция case, вычисляющая тип этого элемента каталога, и переход на новую строку. Синтаксис case не должен вызвать затруднений — сначала вычисляется условное выражение, затем выполняется поиск совпадения с одним из возможных вариантов и производятся соответствующие действия. После выхода из цикла "поток" каталога закрывается.

Результат проверки этого скрипта показан на рис.2.2.

Рис.2.2. Обработка элементов каталога
Рис.2.2. Обработка элементов каталога
Рис.2.2. Обработка элементов каталога

2. Некоторые особенности практического использования Guile

2.1. Функции, как тип данных

В Scheme (соответственно и в Guile) реализован упрощённый подход к представлению функций (процедур), заключающийся в том, что процедура считается одним из базовых типов данных наряду с символьными, строковыми и числовыми значениями. Это означает, что значения типа "процедура" могут передаваться и сохраняться в переменных так же, как, например, строки и числа. Рассматривая функциональные свойства стандартной Scheme-процедуры open-file, нам следует помнить о том, что это всего лишь предварительно определённая переменная глобального уровня с именем open-file, которая содержит процедуру, выполняющую требуемые операции открытия файла в соответствии со стандартом R5RS.

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

В Scheme абсолютно все имена размещаются в едином обобщённом пространстве имён, что с одной стороны предоставляет почти неограниченную свободу, а с другой стороны заставляет очень внимательно относиться к именованию собственных переменных и процедур. То есть, вы можете переопределить стандартное имя Scheme-процедуры, если по какой-либо причине оно вас не устраивает (слишком длинное, не совсем понятное и т.п.). Скажем, если надоело вам часто набирать имя функции call-with-current-continuation, предназначенной для сохранения текущего контекста, необходимого для продолжения вычислений, то вы можете определить более короткое и удобное имя для данной процедуры; другими словами: определить новую переменную и присвоить ей значение предопределённой переменной типа "процедура":

(define call-cc call-with-current-continuation)

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

2.2. Флаги командной строки и мета-ключ

Флаги командной строки интерпретатора Guile позволяют программисту определять разнообразные операции со скриптами. Но механизм вызова скриптов, регламентируемый стандартом POSIX, позволяет задать только один аргумент в строке #!-идентификации после записи полного путевого имени выполняемого файла guile, да к тому же накладывает определённые ограничения на длину этого аргумента. Предположим, что у нас имеется скрипт, записанный следующим образом:

#!/usr/bin/guile -e main -s
!#
(define (main args)
  (map (lambda (arg) (display arg) (display " "))
    (cdr args))
  (newline))

Если вызов скрипта содержит ключ -e, после которого указано имя процедуры, являющейся точкой входа в программу (то есть, эта процедура начинает выполняться самой первой сразу после запуска скрипта), то Guile вызовет эту процедуру с передачей (command-line) в качестве аргумента для неё. То есть, скрипту, который использует ключ -e, нет необходимости в явной форме обращаться к процедуре (command-line) в своём коде. Поэтому смысл того, что мы намерены сделать понятен: загрузить файл этого скрипта, затем вызвать функцию main с передачей в неё аргументов командной строки. Но здесь нас подстерегает неудача — система будет интерпретировать всё, что записано после полного путевого имени guile, как один аргумент — то есть, как строку "-e main -s" — это совсем не то, что нам нужно, да и работать такой скрипт не будет с диагностикой "неопознанный ключ" (см.рис.2.3).

Рис.2.3. Проблема при записи нескольких флагов в скрипте
Рис.2.3. Проблема при записи нескольких флагов в скрипте
Рис.2.3. Проблема при записи нескольких флагов в скрипте

В качестве "обходного маневра" используется мета-ключ (meta-switch) — символ "обратный слэш" (\), который позволяет задавать произвольное количество флагов интерпретатора Guile.

Если первым аргументом, передаваемым Guile, является \, то интерпретатор откроет файл скрипта с именем, указанным после символа \, выполнит синтаксический разбор (парсинг) аргументов, начиная со второй(!) строки этого файла, и подставит эти аргументы вместо мета-ключа \.

Изменим код нашего неправильного скрипта следующим образом:

#!/usr/bin/guile \
-e main -s
!#
(define (main args)
  (map (lambda (arg) (display arg) (display " "))
    (cdr args))
  (newline))

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

./guile-cla abc def xyz

Вот что происходит при этом:

Система распознаёт лексему #! в самом начале файла и переписывает командную строку следующим образом:

/usr/bin/guile \ ./guile-cla abc def xyz

Это действие вполне соответствует стандарту POSIX.

Когда Guile обнаруживает следующие элементы командной строки \ ./guile-cla, он открывает файл ./guile-cla, находит в нём три аргумента (во второй строке) -e main -s и подставляет их вместо мета-ключа \. После этого командная строка приобретает такой вид:

/usr/bin/guile -e main -s ./guile-cla abc def xyz

Далее Guile нормально, без ошибок обрабатывает эти три элемента (два флага и имя процедуры) по отдельности; загружает указанный после флага -s ./guile-cla, как файл с исходным кодом Scheme (воспринимая первые три строки, как комментарий, — об этом мы уже говорили в первой части статьи), а затем начинает выполнять приложение с процедуры main. Результат выполнения можно наблюдать на рис.2.4.

Рис.2.4. Правильная передача нескольких флагов из скрипта
Рис.2.4. Правильная передача нескольких флагов из скрипта
Рис.2.4. Правильная передача нескольких флагов из скрипта

3. Заключение

И всё-таки с помощью Guile можно писать полезные утилиты — в основном, консольные, предназначенные для обработки разношёрстных данных, для синтаксического анализа конфигурационных файлов и прочих задач подобного рода. И Scheme (Lisp) — не самый худший язык программирования; это один из многих инструментов, которыми следует пользоваться.

В первой статье, открывающей цикл, был представлен общий обзор Guile. Во второй статье рассматривалось практическое применение интерпретатора. Третья и четвёртая статьи посвящены взаимодействию Guile с языком программирования C. В пятой, заключительной статье цикла будут описаны дополнительные средства Guile.


Ресурсы для скачивания


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, Linux
ArticleID=509526
ArticleTitle=Guile — универсальный инструмент программирования. Часть 2. Как с ним обращаться
publish-date=08052010