|
Глава 1. 32-разрядные
консольные приложения
Джим Мишель
Высушенное чучело DOS красуется ныне
на стенке Win32 в качестве второстепенного API. Как же теперь бедному хакеру
создать текстовый фильтр, запускаемый из командной строки? Добрая фея POSIX
взмахивает волшебной палочкой… Дзынь! DOS на глазах превращает ся в консольное
приложение, вызывая мучительное ощущение deja vu.
В течение многих лет Linux, Windows, Macintosh
и другие графические пользовательские интерфейсы (GUI) оставались излюбленной
темой компьютерной прессы. Когда основное внимание уделяется разработке
приложений для GUI, бывает трудно вспомнить о том, что существует и другой
мир - мир средств командной строки, которые выполняют пакетные вычисления
с минималь ным вводом информации от пользователя. Пусть такие программы
выглядят не слишком эффектно - несомненно, они приносят немалую пользу.
Скажем, банки обрабатывают сведения о ваших чеках, вкладах и ссудных платежах
ночью, в пакетном режиме. Страховые и кредитные компании, а вместе с ними
и другие бесчисленные учреждения тоже обновляют информацию по ночам. Нужны
ли им для этого красивые среды GUI? Спросите своего кассира в банке. Или
попробуйте угадать сами.
Возможности средств командной строки отнюдь
не ограничиваются финансовыми расчетами на «крутом железе». Несколько таких
программ входит в комплект Windows 95, среди них - ATTRIB, DISKCOPY, FORMAT,
FDISK, SORT и XCOPY. Они присутствуют даже в Delphi - при самом поверхностном
просмотре каталога BIN там можно найти компиляторы ресурсов (BRC32.EXE
и BRCC32.EXE), компилятор языка Паскаль (DCC32.EXE) и другие программы.
Консольные приложения
В Windows 95 и Windows NT существуют консольные
приложения - программы, которые не пользуются услугами GUI, а
работают в окне так называемого «сеанса DOS». Хотя эти приложения не обладают
собственными окнами, они могут пользоваться всем Windows API и полным
32-разрядным адресным пространством Windows (включая виртуальную память).
В Windows 3.1 ситуация была иной - GUI-программы могли работать со всем
адресным пространством Windows, а программы DOS ограничивались нижними
640 Кбайт.
В прошлом DOS-приложения обходили ограничение
в 640 Кбайт с помощью так называемых расширителей DOS, которые поддерживали
такие стандарты, как DPMI (DOS Protected Mode Interface) и VCPI (Virtual
Control Program Interface). 16-разрядный расширитель позволял работать
с 16 Мбайт памяти. Реже встречались 32-разрядные расширители, которые открывали
доступ к полному 32-разрядному адресному пространству, а иногда даже поддерживали
виртуальную память. Проблема расширителей DOS заключается в том, что все
они (даже в самом лучшем исполнении) остаются «хакерством». На многих компьютерах
расширители DOS работали недостаточно надежно, кроме того, некоторые из
них отказывались работать в DOS-сеансах Windows.
В свою очередь консольныеприложения для
Windows 95 - всего лишь Windows-программы, не имеющие окон. Для них не
требуются специальные программные расширители, и консольные приложения
гарантированно работают на любом компьютере с Windows 95 или Windows NT.
Итак, мы получаем доступ ко всей памяти,
но зато лишаемся GUI. Возникает вопрос - что делать дальше?
Фильтры
Вероятно, из всех средств командной строки
на персональных компьютерах чаще всего встречаются программы, принадлежащие
к широкой категории «фильтров». Фильтром может быть все, что угодно, -
от простейшего счетчика строк до сложного компилятора (например, компилятора
языка Паскаль из Delphi), утилиты сортировки или программы пакетных вычислений.
Все фильтры построены на одном принципе:
они вызываются из командной строки и получают аргументы, в которых задаются
параметры их работы, а также имена входных и выходных файлов. Фильтр читает
входные данные, выполняет некоторые вычисления (зависящие от параметров,
указанных в командной строке) и записывает результат в выходной файл.
Фильтры обычно не работают с мышью и вообще
очень редко взаимодействуют с пользователем. Если же фильтр все-таки получает
информацию от пользователя, то для этого применяется простейший текстовый
интерфейс. Вывод, как правило, ограничивается информацией о ходе процесса
(«Working, please wait…»), сообщениями об ошибках и завершающим сообщением
«Done».
В этой главе мы напишем на Delphi относительно
простую программу -фильтр, построив при этом «каркас», на основе которого
можно будет легко создавать другие фильтры. Попутно мы узнаем кое-что о
хранилище объектов Delphi, многократном использовании кода и (содрогнитесь
от ужаса) процессно-ориентированном программировании.
Замечание
Ирония судьбы - всего три года назад я
преподавал программирование для Windows DOS-программистам и рассказывал
им о том, как отказаться от традиционного процессно-ориен тированного мышления
и войти в широкий мир управляемых событиями Windows-программ. С появлением
визуальных средств разработки - таких как Visual Basic и Delphi - многие
новички сразу начинают с событийного программирования и даже не умеют писать
процессно-ориентированные средства командной строки. А теперь я рассказываю
вам о том, как от событийного программирования вернуться к процессно-ориентированному.
Plus зa change.
Единственный «плюс» заключается в том,
что программист, привыкший работать с событиями, без особых трудностей
поймет процессно-ориентированный код. Обратное, к сожалению, неверно.
Консольные приложения
на Delphi
Хотя Delphi позволяет создавать консольные
приложения, документация хранит подозрительное молчание по поводу того,
как именно это делается. Как ни удивительно, среди превосходных примеров,
поясняющих многие аспекты программирования на Delphi, нет ни одного консольного
приложения. К счастью, создать консольное приложение на Delphi не так уж
сложно, хотя при этом желательно знать пару тонких моментов. (Метод проб
и ошибок - не самый эффективный способ обучения!)
Простейшее консольное приложение
- это, конечно же, программа «Hello World». Выглядит она не особо эффектно,
но обычно я начинаю освоение всех новых программных средств именно с нее.
Дело в том, что с помощью «Hello World» можно кое-что узнать о новой среде,
не заботясь о содержании программы. После того как мы напишем на Delphi
простейшее консольное приложение, его код можно будет отправить в хранилище
объектов и пользоваться им как отправной точкой для создания других аналогичных
проектов.
Hello, Delphi
Прежде всего создайте новое приложение (File
<>New Application). Для начала нужно изменить некоторые параметры
проекта и сообщить Delphi о том, что мы создаем именно консольное приложение.
Выполните команду Projectд Options и затем на вкладке Linker диалогового
окна Project Options установите флажок Generate Console Application, после
чего сохраните внесенные изменения кнопкой OK.
Поскольку у консольного приложения нет
главной формы (и, если уж на то пошло, вообще никаких форм), необходимо
удалить форму Form1, которая автоматически появилась при создании
нового приложения. Выполните команду FileдRemove From Project; когда появится
диалоговое окно Remove From Project, выделите строку, содержащую имена
Unit1 и Form1, и нажмите кнопку OK. Если откроется окно сообщения
с предложением сохранить изменения в модуле Unit1, нажмите кнопку
No. В оставшемся окне Delphi нет ничего, кроме инспектора объектов, - нет
ни форм, ни модулей. Где же писать код программы?
Остается лишь файл с исходным текстом проекта.
Выполните команду ViewдProject Source. Delphi откроет окно текстового редактора
с файлом PROJECT1.DPR. Именно этот файл мы модифицируем, чтобы создать
первое консольное приложение. Перед тем как продолжать работу над программой,
выполните команду File <>Save и сохраните проект под именем HELLO.DPR.
В редакторе измените исходный текст проекта
в соответствии с листингом 1.1 и сохраните свою работу. Нажмите клавишу
F9, чтобы откомпилировать и запустить программу.
Листинг 1.1. Программа Hello, Delphi
{
HELLO.DPR - Простейшее консольное приложение Delphi
Автор: Джим Мишель
Дата последней редакции: 04/05/97
}
{$APPTYPE CONSOLE}
program Hello;
uses Windows;
begin
WriteLn ("Hello, Delphi");
Write ("Press Enter...");
ReadLn;
end.
Строка {$APPTYPE CONSOLE} в листинге
1.1 является директивой компилято ра и сообщает Delphi о том, что создаваемое
приложение является консольным. Она должна присутствовать в начале
любого консольного приложения. Эта директива включается только в программы
- она не нужна в модулях или библиотеках динамической компоновки (DLL).
Ключевое слово uses нашей программе, вообще говоря, не нужно (мы
здесь не обращаемся к функциям Windows API), но по какой-то загадочной
причине Delphi не любит сохранять проекты без секции uses (см. мое
замечание о методе проб и ошибок). Включение модуля Windows не принесет
никакого вреда и говорит вовсе не о том, что модуль подключается к программе,
а лишь о том, что Delphi просмотрит его, если не сможет найти какой-нибудь
идентификатор в текущем модуле.
Оставшаяся часть программы проста до очевидного.
Строка «Hello, Delphi» выводится на консоль (то есть на экран), после чего
вам будет предложено нажать Enter. Я включил сюда ожидание ввода лишь потому,
что без него Delphi на долю секунды выведет окно консоли (сеанса DOS),
запустит программу и сразу же закроет окно. Ожидание нажатия Enter позволяет
убедиться в том, что программа действительно работает.
Сохранение шаблона
программы
Создать консольное приложение не так уж сложно,
но при этом все же приходится помнить о некоторых нюансах. Поэтому, вместо
того чтобы каждый раз строить приложение на пустом месте (и благополучно
забывать об этих нюансах), давайте сохраним программу Hello в хранилище
объектов, чтобы у нас появилась отправная точка для создания других консольных
приложений.
С помощью Windows Exploder (в Windows NT
3.51 мы любили называть эту программу File Mangler) создайте подкаталог
ConsoleApp в подкаталоге Objrepos основного каталога Delphi.
Если вы установили Delphi со стандарт ными параметрами, полный путь будет
выглядеть так:
C:\Program Files\Borland\Delphi 3\Objrepos\ConsoleApp
Затем выполните команду Project <>
Save Project As из меню Delphi и сохрани те проект под именем ConsoleApp.dpr
(хорошая штука - длинные имена!) в только что созданном каталоге.
После того как проект будет сохранен, включите
его в хранилище командой Project д Add to Repository, после чего заполните
диалоговое окно Add to Repository (см. рис. 1.1).
Рис. 1.1. Шаблон консольного приложения
добавляется в хранилище
После того как проект будет добавлен в
хранилище, попробуйте выполнить команду File <> New в меню Delphi,
перейдите на вкладку Projects в диалоговом окне New Items и дважды щелкните
на значке Console Application. Delphi предложит указать каталог и создаст
новый проект с параметрами, заранее настроенными для консольного приложения.
Замечание
Я так и не решил, стоит ли держать свои
объекты непосредственно в каталогах хранилища Delphi. Это довольно удобно,
но любое обновление версии Delphi может обернуться неприятностями. Скорее
всего, при обновлении каталог Objrepos будет удален - вместе со
всеми замечательными объектами, которые в нем находятся. Вам придется вручную
сохранять их перед каждым обновлением.
Существует и другой вариант - создать собственный
каталог-хранилище, не принадлежа щий основному каталогу Delphi. В любом
случае при обновлении Delphi вам придется заново включать объекты в хранилище,
но отдельный каталог по крайней мере защитит ваши проекты от случайного
удаления.
Консольный ввод/вывод
При запуске консольного приложения с окном
консоли автоматически связываются стандартные текстовые файлы Input
и Output. В результате процедуры ReadLn и WriteLn
работают именно так, как вы ожидаете, - равно как процедуры Eof,
Eoln,
Read, Write и все остальные средства ввода/вывода для текстовых
файлов.
Существует целый ряд консольных функций
ввода/вывода, которые время от времени оказываются полезными. К сожалению,
эти функции определены в консольном интерфейсе Windows, и в Delphi не существует
никакой удобной оболочки, которая скрывала бы от нас все отвратительные
техниче ские подробности (кстати, напрашивается отличный shareware-проект
для талантливого программиста - класс Delphi, инкапсулирующий консольный
интерфейс Windows). Консольный интерфейс Windows сам по себе требует отдельной
главы, поэтому сейчас я обойду его деликатным молчанием. Если вы захотите
побольше узнать о PeekConsoleInput, WriteConsole и других
функциях консольного API, обратитесь к разделу Console Reference файла
WIN32.HLP из подкаталога Help Delphi. Программа установки
не создает ссылку на этот файл, так что вам придется самостоятельно найти
и загрузить его.
Из-за недостатка места для полноценного
обсуждения консольного API работа с консолью в нашем приложении будет ограничена
стандартными функциями файлового ввода/вывода. Поймите меня правильно -
функции консольного API могут принести пользу во многих приложениях, но
только не в тех, которые обычно пишутся как консольные. Да, я знаю, что
это звучит довольно странно, но, похоже, консольный API больше подходит
для GUI-программ, управляющих консольными окнами, а не для обычных консольных
приложений, которые работают сами по себе.
Возможности консольных приложений не ограничиваются
унылым текстовым интерфейсом. Поскольку у вас имеется полный доступ к Windows
API, вы можете отображать окна сообщений и диалоговые окна, управлять работой
других окон и даже создавать другие консольные окна из своего приложения.
Программа-фильтр
на Delphi
Мы научились создавать консольные приложения,
теперь пора воспользовать ся полученными знаниями на практике. Оставшаяся
часть этой главы посвящена написанию фильтров как разновидности консольных
приложений. После краткого знакомства с фильтрами мы поговорим об анализе
командных строк и эффективной работе с файлами. Нам придется отхватить
изрядный кусок от стандартной runtime-библиотеки Delphi, поэтому на подробное
обсуждение каждой функции не хватит времени. Помните, что электронная документа
ция - ваш лучший помощник, почаще обращайтесь к ней.
Базовая программа-фильтр
Как я упоминал в начале этой главы, программы-фильтры
обычно получают командную строку с параметрами и именами входных/выходных
файлов, обрабатывают входную информацию в соответствии с полученными параметрами
и создают выходной файл.
Столь общее описание оставляет более чем
достаточно возможностей для импровизации. Например, программа для подсчета
строк может получать имена сразу нескольких файлов (в том числе и файловые
маски), а при указании некоторого параметра - считать не только текстовые
строки, но также слова и символы или даже выдавать распределение слов и
символов по относительной частоте. В более сложной программе результат
работы может представлять собой отдельный файл, полученный преобразованием
одного или нескольких входных файлов, или сразу несколько файлов, полученных
в результате обработки одного входного файла.
Несмотря на все различия в сложности, фильтры
обладают рядом общих функций. Все они обрабатывают содержимое командной
строки, читают входные файлы и записывают выходные. Разные программы существенно
отличаются друг от друга лишь промежуточной стадией обработки. Благодаря
этой общности можно создать группу функций, которые реализуют основные
задачи фильтров и позволяют быстро создавать нестандартные фильтры, для
чего потребуется лишь указать синтаксис командной строки и написать код
для стадии «обработки». Ввод, вывод, анализ командной строки - все это
уже присутствует. Программа-фильтр хранится в виде концентрата, остается
лишь добавить воду... то есть обработку.
Обработка командной
строки
На первый взгляд в обработке командной строки
нет ничего сложного. У вас имеется текстовая строка, из нее нужно выделить
имена файлов и параметры, после чего соответствующим образом настроить
переменные программы. Не перестаю удивляться, насколько сложной
оказывается такая простая задача. К счастью, Object Pascal содержит две
стандартные функции, ParamCount и
ParamStr, которые немного
облегчают работу.
ParamCount просто возвращает количество
параметров, переданных в командной строке. Следовательно, для командной
строки «MyFilter file1.txt file2.txt» будет возвращено значение
2. Функция не включает в число параметров имя самой программы.
ParamStr получает целое число и
возвращает строку, которая соответствует аргументу с заданным номером.
Например, для приведенной выше командной строки оператор вида
WriteLn(ParamStr (1));
выведет текст «file1.txt» (разумеется,
без кавычек).
Если вызвать ParamStr с параметром
0, возвращается строка с полным путем и именем текущей выполняемой программы.
Программа Params (см. листинг 1.2) показывает,
как работать с ParamCount и ParamStr. Чтобы создать эту программу,
выполните в меню Delphi команду FileдNew, выберите на вкладке Projects
диалогового окна New Items значок Console Application и задайте каталог
для нового приложения. Не забудьте сохранить проект под именем Params.dpr,
прежде чем приступать к его изменению.
Листинг 1.2. Программа Params
{
PARAMS.PAS - пример использования функций ParamCount и ParamStr.
Автор: Джим Мишель
Дата последней редакции: 04/05/97
}
{$APPTYPE CONSOLE}
program Params;
uses Windows;
Var
i : Integer;
begin
WriteLn ("Program: ", ParamStr (0));
WriteLn ("ParamCount = ", ParamCount);
WriteLn ("Parameters");
WriteLn ("-----");
for i := 1 to ParamCount do
begin
WriteLn (ParamStr (i));
end;
Write ("Press Enter...");
ReadLn;
end.
Если вам захочется протестировать программу
из Delphi, выполните команду Run д Parameters и введите командную строку.
Для реализации приведен ного выше примера следует ввести в диалоговом окне
Run parameters строку «file1.txt file2.txt» (без кавычек).
Не правда ли, просто? К сожалению, не совсем.
В старое доброе время DOS и Windows 3.1 все было действительно просто.
Но потом появились длинные имена файлов, которые к тому же могли содержать
пробелы. Возникает проблема. Видите ли, функции ParamCount и ParamStr
предполагают, что аргументы командной строки разделяются пробелами. Все
идет замечательно, пока имена файлов не содержат пробелов, но попробуйте-ка
ввести такую командную строку:
params c:\program files\borland\delphi
3\readme.txt
Функция ParamCount возвращает 3,
а параметры с ее точки зрения выглядят так:
c:\program
files\borland\delphi
3\readme.txt
Получается совсем не то, что мы
ожидали увидеть! (Пожалуй, длинные имена файлов не всегда хороши. Иногда
они вызывают сплошные огорчения.)
Я не стану углубляться в обсуждение этой
темы. Если вам захочется побольше узнать о проблеме и ее возможных решениях
(ни одно из которых, кстати говоря, нельзя признать удовлетворительным
- спасибо тебе, Microsoft), обратитесь к книге Лу Гринзо (Lou Grinzo) «Zen
of Windows 95 Programming». Книга посвящена программированию на C и C++
для Windows 95, но в ней найдется много информации, полезной для всех программистов,
особенно о методах написания корректно работающих программ. Эта книга входит
в тройку лучших книг по программированию, которые мне приходилось читать,
наравне с «Writing Solid Code» и «Debugging the Development Process» -
обе книги написаны Стивом Магуайром (Steve Maguire) и опубликованы издательством
Microsoft Press.
Единственное работоспособное (хотя
и не удовлетворительное) решение - потребовать, чтобы имена файлов, содержащие
пробелы, заключались в кавычки. При этом командная строка из предыдущего
примера приобретает следующий вид:
params "c:\program files\borland\delphi
3\readme.txt"
Конечно, можно потребовать, чтобы пользователи
всегда указывали короткую версию имени, но уж лучше ввести кавычки, чем
мучиться со строкой типа
params "c:\progra~1\borland\delphi~1\readme.txt"
Параметры командной
строки
Большинству программ командной строки параметры
передаются прямо в строке. Иногда встречаются программы, получающие параметры
из переменных окружения или конфигурационных файлов, а также гибриды, получающие
параметры из командной строки или конфигурационного файла, имя которого
указывается в командной строке. Чтобы не увязнуть в получении
параметров, мы проигнорируем конфигурационные
файлы и переменные окружения, сосредоточив все внимание на параметрах командных
строк.
Вам наверняка приходилось пользоваться
средствами командной строки (скажем, командой DIR), в которых для параметров
используется префикс - косая черта (/). Например, чтобы вывести список
файлов текущего каталога и всех его подкаталогов, следует ввести DIR
/S. Кроме того, во многих программах в качестве префикса используется
дефис (он же знак «минус», -). Оба символа распространены достаточно широко,
и во многих программах можно указывать любой из них.
С другой стороны, имена файлов задаются
множеством способов в зависимости от конкретной программы. Например, COPY
позволяет задавать имена входного и выходного файла без префиксов. Следовательно,
строка COPY FILE1 FILE2 скопирует содержимое FILE1 в FILE2.
Программа MAKE фирмы Borland, напротив, требует задать для имени входного
файла префикс -f. Так, для обработки файла BUILD.MAK следует
ввести команду MAKE -fbuild.mak.
Система, принятая в MAKE, оказывается более
простой - здесь к параметрам относится вс?. Каждый параметр командной
строки отделяется от других хотя бы одним пробелом, а имена файлов обрабатываются
наравне с прочими параметрами - никаких исключений не предусмотрено. Именно
такую модель мы реализуем в своем фильтре.
Параметры командной строки обычно делятся
на четыре категории: переключатели, числа, строки и имена файлов. Переключатель
просто включает или выключает какой-то режим. Например, в текстовом фильтре
может быть предусмотрен переключатель для перевода всех символов в верхний
регистр. Числа могут быть как целыми, так и вещественными. Задавать
их можно несколькими способами, чаще всего встречается десятичное и шестнадцатеричное
представление. Строки похожи на имена файлов, однако для последних
часто предусмотрена проверка правильности синтаксиса.
Универсальный анализатор
командных строк
Если меня что и раздражает в программировании,
так это необходимость в десятый (или сотый) раз писать код для выполнения
одной и той же задачи. С анализом командных строк дело обстоит именно так
- он необходим во всех фильтрах без исключения, но после того как вы напишете
этот код пару раз, задача становится на редкость скучной . Поэтому
я и постарался создать некий обобщенный анализатор, который ценой минимальных
усилий с моей стороны обрабатывает командную строку и присваивает нужные
значения переменным. Благодаря этому я могу уделить больше внимания самому
фильтру (то есть основной задаче), а не второстепенному анализатору.
Обобщенный анализатор командных строк -
это вам не фунт изюма, и даже самый тривиальный вариант потребует немалых
усилий. Анализатор из нашего примера обладает минимальными возможностями,
но во многих приложениях этого будет вполне достаточно.
Основная идея заключается в том, чтобы
определить префиксы параметров, указать тип каждого параметра и задать
значения по умолчанию. Структура, содержащая всю эту информацию, передается
анализатору, который обрабатывает командную строку и присваивает значения
найденным параметрам. Если при обработке строки происходит ошибка (скажем,
обнаружи вается неизвестный параметр или там, где должен стоять переключатель,
оказывается число), анализатор выдает сообщение об ошибке, прерывает работу
и уведомляет вызывающую функцию. Ну как, просто? Да, просто сказать… запрограммировать
несколько сложнее.
Информация об отдельном параметре хранится
в виде записи OptionsRec, описанной в листинге 1.3. В нем приведен
полный исходный текст всего модуля CmdLine. Создайте новый файл в редакторе,
введите и сохраните код под именем CMDLINE.PAS.
Листинг 1.3. Модуль CmdLine
{ CMDLINE.PAS -
Анализатор командной строки Автор: Джим Мишель
Дата последней редакции: 04/05/97
}
unit cmdline;
interface
type
OptionType = (otBool, otInt, otString,
otFilename);
pOptionRec = ^OptionRec;
OptionRec = record
OptionChar : char;
case Option : OptionType of
otBool : (OnOff : Boolean);
otInt : (Value : Integer);
otString : (Param : ShortString);
otFilename : (Filename : ShortString);
end;
pOptionsArray = ^OptionsArray;
OptionsArray = Array [1..1] of OptionRec;
{
GetOptionRec - возвращает указатель на
запись из передаваемого массива параметров
Options, соответствующую заданному префиксу.
Возвращает Nil, если префикс отсутствует в массиве.
}
function GetOptionRec
(
Options : pOptionsArray;
nOptions : Integer;
OptionChar : char
) : pOptionRec;
{
ProcessCommandLine - обрабатывает
командную строку в соответствии
со списком параметров, переданным в массиве
Options. Возвращает True при успешном завершении
и False - в случае ошибки.
}
function ProcessCommandLine
(
Options : pOptionsArray;
nOptions : Integer
) : Boolean;
implementation
uses SysUtils;
{
GetOptionRec - возвращает указатель на
запись из передаваемого массива параметров
Options,
соответствующую заданному префиксу.
Возвращает Nil, если префикс отсутствует в
массиве.
}
function GetOptionRec
(
Options : pOptionsArray;
nOptions : Integer;
OptionChar : char
) : pOptionRec;
var
i : Integer;
begin
Result := Nil;
for i := 1 to nOptions do begin
if (Options^[i].OptionChar = OptionChar)
then begin
Result := @Options^[i].OptionChar;
Break;
end;
end;
end;
{
ProcessBool
Определяет состояние параметра-переключателя (вкл/выкл).
Если в Param передается пустая строка,
параметр считается включенным (+).
В противном случае строка должна начинаться со
знака + или -,в соответствии с которым присваивается
значение переменной OnOff.
}
function ProcessBool
(
Param : String;
var OnOff : Boolean
) : Boolean;
begin
Result := True;
if (Length (Param) = 0) then begin
OnOff := True;
Exit;
end;
case Param[1] of
"+" : OnOff := True;
"-" : OnOff := False;
else begin
WriteLn ("Error: + or - expected");
Result := False;
end;
end;
end;
{
ProcessInt
Извлекает целое число из переданного параметра
командной строки.
}
function ProcessInt
(
Param : String;
var Value : Integer
) : Boolean;
begin
if (Length (Param) = 0) then begin
Result := False;
WriteLn ("Error: integer expected");
Exit;
end;
Result := True;
try
Value := StrToInt (Param);
except
WriteLn ("Error: integer expected");
Result := False;
end;
end;
{
ProcessString
Копирует переданную строку в переменную Option.
Проверка ошибок не выполняется, а пустая строка считается
допустимым параметром.
}
function ProcessString
(
Param : String;
var Option : ShortString
) : Boolean;
begin
Option := Param;
Result := True;
end;
{
ProcessFilename
Извлекает имя файла из переданного параметра
командной строки. В настоящей реализации функция
просто вызывает ProcessString и копирует
строковый параметр в Filename. Возможно, в
будущих версиях она будет проверять, является ли
строка допустимым именем файла, или же будет
использоваться для преобразования короткого имени
в полное, включающее путь.
}
function ProcessFilename
(
Param : String;
var Filename : ShortString
) : Boolean;
begin
Result := ProcessString (Param, Filename);
end;
{
CheckParam
Проверяет, принадлежит ли аргумент командной
строки Param заданному списку параметров.
Если префикс будет признан допустимым, обрабатывает
параметр в соответствии с его типом
(логическим, целым, строковым или файловым).
Возвращает True при правильной обработке и
сохранении параметра и False в противном случае.
}
function CheckParam
(
Param : String;
Options : pOptionsArray;
nOptions : Integer
) : Boolean;
var
Rec : pOptionRec;
Option : String;
begin
Result := False;
if (Param[1] in ["-", "/"])
then begin
if (Length (Param) < 2) then begin
WriteLn ("Invalid option");
end
else begin
Rec := GetOptionRec (Options, nOptions, Param[2]);
if (Rec <> Nil) then begin
Option := Copy (Param, 3, Length (Param) - 2);
case Rec^.Option of
otBool :
Result := ProcessBool (Option, Rec.OnOff);
otInt :
Result := ProcessInt (Option, Rec^.Value);
otString :
Result := ProcessString (Option, Rec^.Param);
otFilename :
Result := ProcessFilename (Option, Rec^.Filename);
else
WriteLn ("Invalid option specification: ", Param[2]);
end;
end
else begin
WriteLn ("Invalid option character: ", Param[2]);
end;
end;
end
else begin
WriteLn ("Error: options must start with - or /");
end;
end;
{
ProcessCommandLine
По заданному списку префиксов и типов
параметров проверяет каждый аргумент
командной строки и соответствующим образом
присваивает значения информационным
полям записей массива Options.
Возвращает True, если все параметры были
успешно обработаны и сохранены.
}
function ProcessCommandLine
(
Options : pOptionsArray;
nOptions : Integer
) : Boolean;
var
ParamNo : Integer;
begin
Result := True;
for ParamNo := 1 to ParamCount do begin
if (Not CheckParam (ParamStr (ParamNo), Options, nOptions)) then begin
Result := False;
Exit;
end;
end;
end;
end.
Перечисляемый тип OptionType описывает
различные виды параметров, о которых известно функции ProcessCommandLine.
Запись OptionRec содержит три поля: префикс, тип параметра и вариантную
часть, в которой хранится значение данного параметра (если вы незнакомы
с вариантными записями, просмотрите раздел справки с соответствующей информацией
или купите простейший учебник по Паскалю в ближайшем книжном магазине).
Запись OptionRec оказывается не
слишком эффективным решением, поскольку все записи независимо от типа параметра
имеют максимальный размер из всех возможных вариантов. Размер типа ShortString
равен 256 байтам, поэтому большинство записей будет занимать гораздо больше
места, чем действительно необходимо. Существует несколько способов решения
этой проблемы, самый простой из них - использовать указатели
на строки (вместо самих строк) для строковых и файловых типов. Я не реализовал
эту возможность, поскольку она требует дополнительного кодирования.
Другая проблема тоже связана с типом ShortString.
Самая длинная строка, которая может храниться в переменной типа ShortString,
состоит из 255 символов, тогда как максимальная длина пути в Windows оказывается
несколько длиннее (260 байт). Я рассчитывал воспользоваться типом Delphi
AnsiString (то есть «длинной строкой»), но длинные строковые типы
не могут входить в вариантную часть записи. И снова самым очевидным решением
будет использование указателей.
Несмотря на эти проблемы, модуль CmdLine
способен принести немало пользы. Дополнительные расходы памяти не особенно
страшны, поскольку в большинстве программ используется совсем немного параметров,
и нас уже не страшит дурацкое ограничение в 64 Кбайт на размер статических
данных. (Помните, мы живем в обширном 32-разрядном мире!) С ограничением
на длину имени дело обстоит посложнее, но лично у меня найдется не так
уж много знакомых, которым захотелось бы вводить 256-символьный путь в
командной строке (точнее, таких вообще не найдется).
Модуль CmdLine содержит две функции, которые
могут вызываться внешними программами: GetOptionRec и ProcessCommandLine.
Функция GetOptionRec возвращает указатель на запись с заданным префиксным
символом. Если такой записи не существует, GetOptionRec возвращает
Nil. Вся настоящая работа выполняется в функции ProcessCommandLine.
Вы передаете ей массив структур OptionRec, а она анализирует командную
строку и заполняет поля значений для каждого параметра. Если ProcessCommandLine
удается без ошибок обработать все аргументы командной строки, она возвращает
True. Если в какой-то момент произойдет ошибка, функция немедленно
прекращает работу, выдает сообщение об ошибке и возвращает значение False.
Тестирование модуля
CmdLine
Теперь мы проверим, как работают функции анализа
командной строки, с помощью тестовой программы. Создайте новое приложение
на основе шаблона Console Application. Сохраните новый проект под именем
FILTER.DPR и скопируйте файл CMDLINE.PAS (листинг 1.3) в
соответствующий каталог. Затем выполните команду File д Add to Project,
чтобы включить модуль CmdLine в созданный проект.
Проект Filter предназначен для проверки
модуля CmdLine, а также модуля файлового ввода/вывода, которым мы займемся
далее. После завершения работы над модулями их окончательные версии будут
помещены в хранили ще, и у нас появится шаблон для создания фильтров.
Для проверки модуля CmdLine нам понадобится
массив с информацией о параметрах и фрагмент кода, в котором вызывается
ProcessCommandLine. Тестовая программа (файл FILTER.DPR)
приведена в листинге 1.4.
Листинг 1.4. Программа FILTER.DPR
для тестирования модуля CmdLine
{
FILTER.DPR — основная программа фильтра
Автор: Джим Мишель
Дата последней редакции: 04/05/97
}
{$APPTYPE CONSOLE}
program filter;
uses Windows, CmdLine;
const
nOptions = 4;
Options : Array [1..nOptions] of OptionRec = (
(OptionChar : "i"; Option : otFilename; Filename : ""),
(OptionChar : "o"; Option : otFilename; Filename : ""),
(OptionChar : "n"; Option : otInt; Value : 36),
(OptionChar : "d"; Option : otBool; OnOff : False)
);
var
cRslt : Boolean;
Rec : pOptionRec;
begin
cRslt := CmdLine.ProcessCommandLine (@Options, nOptions);
WriteLn("ProcessCommandLine returned ", cRslt);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "i");
WriteLn ("i = ", Rec^.Filename);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "o");
WriteLn ("o = ", Rec^.Filename);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "n");
WriteLn ("i = ", Rec^.Value);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "d");
WriteLn ("d = ", Rec^.OnOff);
Write("Press Enter...");
ReadLn;
end.
После инициализации таблицы параметров
(это происходит в секции const) вызывается функция ProcessCommandLine,
которая читает аргументы командной строки и сохраняет значения параметров
в таблице. Затем программа выводит результат, возвращенный функцией ProcessCommandLine,
вместе со значени ями всех параметров.
Попробуйте задавать этой программе различные
командные строки. Не ограничивайтесь правильными строками и обязательно
введите несколько неправильных, чтобы убедиться в корректной обработке
ошибок. Могу предложить несколько вариантов:
-iInFile.txt -oOutFile.txt -n995 -d{правильная
строка}
-n8.94 {Error: integer expected}
-x {Invalid option character: x}
Обобщенный анализатор командных строк,
содержащийся в модуле CmdLine, позволяет очень легко получить параметры
нашей программы. Достаточно заполнить таблицу и передать ее функции ProcessCommandLine,
которая и выполнит всю необходимую работу. Все, что от вас требуется, -
проследить за тем, чтобы все необходимые параметры были заданы,
и присвоить значения внутренним переменным программы в соответствии с указанными
параметрами. Поверьте, это намного проще, чем писать отдельный анализатор
для каждой программы.
Несколько слов о
структуре программы
Перед тем как заняться более сложной программой,
давайте перенесем рабочий код из файла проекта (DPR) в отдельный
модуль. Как показывает опыт, смысловой код желательно убрать из файла проекта
и хранить в отдельных модулях. Для этого есть несколько причин.
Самая главная причина заключается в том,
что Delphi время от времени вносит изменения в файл проекта. Я думаю,
что это происходит лишь при переименовании проекта или включении в него
новых модулей, но полной уверенности у меня нет. Я понятия не имею, что
может проделать Delphi с файлом проекта, и мне нигде не попадалась полная
документация по этому вопросу. Будет крайне неприятно, если Delphi изменит
что-то такое, что я считал неизменным. С другой стороны, я могу случайно
убрать из файла проекта то, что Delphi поместит туда по своим личным соображениям.
Даже этой причины для меня вполне достаточно. В то же время Delphi редко
вносит изменения в модули, не связанные с формами (насколько я знаю, это
происходит лишь при переименовании модуля командой File <> Save
As), поэтому я предпочитаю держать свой код в отдельных модулях.
Другая причина - усложнение отладки. Почему-то
у меня возникали трудности с установкой точек прерывания и пошаговым выполнением
кода из DPR-файла.
Наконец, файл проекта - это всего лишь
файл проекта. После знакомства со структурой программ-примеров и общим
подходом Delphi к созданию проектов у меня сложилось впечатление, что DPR-файл
не предназначен для хранения больших объемов выполняемого кода. Файл проекта
объединяет модули для менеджера проекта, а во время выполнения программы
автоматически создает некоторые формы, после чего запускает приложение.
Думаю, с продуктом следует обращаться так, как задумали его разработчики.
Давайте отделим наш рабочий код и сведем
файл FILTER.DPRк единствен ной выполняемой строке. В листинге 1.5
содержится новый файл FILTER.DPR, а в листинге 1.6 - модуль FILTMAIN.PAS,
где теперь находит ся весь смысловой код.
Листинг 1.5. Новый файл проекта
Filter
{$APPTYPE CONSOLE}
program filter;
uses
cmdline in "cmdline.pas",
filtmain in "filtmain.pas",
fileio in "fileio.pas";
begin
DoFilter;
end.
Листинг 1.6. FILTMAIN: основной
рабочий модуль программы Filter
{
FILTMAIN.PAS - основной рабочий модуль
программы Filter.
Автор: Джим Мишель
Дата последней редакции: 04/05/97
}
unit filtmain;
interface
{ DoFilter выполняет всю работу }
procedure DoFilter;
implementation
uses CmdLine;
procedure DoFilter;
const
nOptions = 4;
Options : Array [1..nOptions] of OptionRec = (
(OptionChar : "i"; Option : otFilename; Filename : ""),
(OptionChar : "o"; Option : otFilename; Filename : "")
(OptionChar : "n"; Option : otInt; Value : 36),
(OptionChar : "d"; Option : otBool; OnOff : False)
);
var
cRslt : Boolean;
Rec : pOptionRec;
begin
cRslt := CmdLine.ProcessCommandLine (@Options, nOptions);
WriteLn("ProcessCommandLine returned ", cRslt);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "i");
WriteLn ("i = ", Rec^.Filename);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "o");
WriteLn ("o = ", Rec^.Filename);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "n");
WriteLn ("i = ", Rec^.Value);
Rec := CmdLine.GetOptionsRec (@Options, nOptions, "d");
WriteLn ("d = ", Rec^.OnOff);
Write("Press Enter...");
ReadLn;
end;
end.
Теперь файл проекта содержит именно то,
что он должен содержать, - информацию о проекте и команду «марш!». Весь
код, написанный программистом, вынесен в FILTMAIN.PAS.
Файловые операции
чтения/записи
Разобравшись с анализом командных строк, мы
приступаем к следующей крупной подзадаче - файловому вводу/выводу. Разумеется,
при простейших посимвольных (или построчных) преобразованиях текстовых
файлов можно пользоваться функциями Read и Write (или
ReadLn
и WriteLn) в сочетании с Eof и Eoln. Например, процедура
DoFilter из листинга 1.7 копирует символы из входного файла в выходной,
преобразуя их к верхнему регистру.
Листинг 1.7. Перевод символов в
верхний регистр
procedure DoFilter;
const
nOptions = 2;
Options : Array [1..nOptions] of OptionRec = (
(OptionChar : "i"; Option : otFilename; Filename : ""),
(OptionChar : "o"; Option : otFilename; Filename : "")
);
var
cRslt : Boolean;
iRec : pOptionRec;
oRec : pOptionRec;
InputFile : Text;
OutputFile : Text;
c : char;
begin
cRslt := CmdLine.ProcessCommandLine
(@Options, nOptions);
if (not cRslt) then
Halt;
{ Убедимся в том, что были заданы имена входного
и выходного файлов }
iRec := CmdLine.GetOptionRec (@Options, nOptions, "i");
if (iRec^.Filename = "") then begin
WriteLn ("Error: input file expected");
Halt;
end;
oRec := CmdLine.GetOptionRec (@Options,
nOptions, "o");
if (oRec^.Filename = "") then begin
WriteLn ("Error: output file expected");
Halt;
end;
{ Открываем входной файл - без проверки ошибок}
Assign (InputFile, iRec^.Filename);
Reset (InputFile);
{ Создаем выходной файл - без проверки ошибок}
Assign (OutputFile, oRec^.Filename);
Rewrite (OutputFile);
{ Читаем и преобразуем каждый символ }
while (not Eof (InputFile)) do begin
Read (InputFile, c);
c := UpCase (c);
Write (OutputFile, c);
end;
Close (InputFile);
Close (OutputFile);
end;
У данной версии программы FILTER есть два
недостатка. Во-первых, она еле ползает - словно змея, пробуждающаяся от
зимней спячки. Если у вас найдется мегабайтовый текстовый файл и несколько
свободных минут, убедитесь сами. Во-вторых, она работает только с текстовыми
файлами. Для одноразового приложения сойдет и так, но мы пишем шаблон для
различных
программ, которым может понадобиться работать
и с двоичными файлами. Да и скорость работы не мешало бы повысить. Поэтому
необходимо найти более универсальный и быстрый способ чтения символов (или
байтов) из файла. Нам придется самостоятельно организовать буферизацию;
программа при этом усложняется, но результат стоит затраченных усилий.
Класс TFilterFile из листинга 1.8
предназначен для организации быстрых побайтовых операций с файлами в программах-фильтрах.
Он инкапсулирует все детали буферизации и по возможности избавляет программиста
от необходимости помнить о многочисленных житейских проблемах работы с
файлами (вам остается лишь вызвать Open и Close).
Листинг 1.8. Реализация класса TFilterFile
из файла FILEIO.PAS
{
FILEIO.PAS - Файловый ввод/вывод для программ-фильтров
Автор: Джим Мишель
Дата последней редакции: 04/05/97
}
{$I+} { Использовать исключения для
обработки ошибок }
unit fileio;
interface
type
FileIOMode = (fioNotOpen, fioRead, fioWrite);
BuffArray = array[0..1] of byte;
pBuffArray = ^BuffArray;
TFilterFile = class (TObject)
private
FFilename : String;
F : File;
FBufferSize : Integer;
FBuffer : pBuffArray;
FBytesInBuff : Integer;
FBuffIndx : Integer;
FFileMode : FileIOMode;
function ReadBuffer : boolean;
function WriteBuffer : boolean;
public
constructor Create (AName : String;
ABufSize : Integer);
destructor Destroy; override;
function Open (AMode : FileIOMode) : Boolean;
procedure Close;
function Eof : Boolean;
function GetByte : byte;
function PutByte (b : byte) : boolean;
end;
implementation
{ TFilterFile }
{ Create - подготавливает, но не открывает
файл }
constructor TFilterFile.Create
(
AName : String;
ABufSize : Integer
);
begin
inherited Create;
FFilename := AName;
FBufferSize := ABufSize;
FBytesInBuff := 0;
FBuffIndx := 0;
FFileMode := fioNotOpen;
{ Назначаем, но не открываем }
Assign (F, FFilename);
{ Выделяем память для буфера }
GetMem (FBuffer, FBufferSize);
end;
{ Destroy - закрывает файл (если он открыт)
и уничтожает объект }
destructor TFilterFile.Destroy;
begin
{ Если файл открыт, закрываем его }
if (FFileMode <> fioNotOpen) then begin
Self.Close;
end;
{ Если был выделен буфер, освобождаем его }
if (FBuffer <> Nil) then begin
FreeMem (FBuffer, FBufferSize);
FBuffer := Nil;
end;
inherited Destroy;
end;
{ Open - открыть файл в нужном режиме }
function TFilterFile.Open
(
AMode : FileIOMode
) : Boolean;
var
SaveFileMode : Byte;
begin
Result := True;
SaveFileMode := FileMode; { переменная FileMode определена в модуле System }
{ Пытаемся открыть файл }
try
case AMode of
fioRead : begin
FileMode := 0;
Reset (F, 1);
end;
fioWrite : begin
FileMode := 1;
Rewrite (F, 1);
end;
end;
FFileMode := AMode;
except
Result := False;
end;
FBytesInBuff := 0;
FBuffIndx := 0;
FileMode := SaveFileMode;
end;
{ Close - закрывает файл, при необходимости
сбрасывая буфер }
procedure TFilterFile.Close;
begin
{ Если буфер записи не пуст, записываем его }
if ((FFileMode = fioWrite) and
(FBytesInBuff > 0)) then begin
WriteBuffer;
end;
try
{ Закрываем файл }
System.Close (F);
finally
FFileMode := fioNotOpen;
end;
end;
{ ReadBuffer - читает блок из файла в буфер }
function TFilterFile.ReadBuffer : Boolean;
begin
Result := True;
if (Self.Eof) then begin
Result := False;
end
else begin
try
BlockRead (F, FBuffer^, FBufferSize,
FBytesInBuff);
except
Result := False;
end;
end;
end;
{ GetByte - возвращает следующий байт из файла.
При необходимости читает из файла в буфер }
function TFilterFile.GetByte : byte;
begin
if (FBuffIndx >= FBytesInBuff) then begin
if (not ReadBuffer) then begin
Result := 0;
Exit;
end
else begin
FBuffIndx := 0;
end;
end;
Result := FBuffer^[FBuffIndx];
Inc (FBuffIndx);
end;
{ WriteBuffer - записывает блок из буфера в файл }
function TFilterFile.WriteBuffer : Boolean;
begin
Result := True;
try
BlockWrite (F, FBuffer^, FBytesInBuff);
except
Result := False;
end;
if (Result = True) then begin
FBytesInBuff := 0;
end;
end;
{ PutByte - заносит байт в буфер. При необходимости записывает буфер в файл }
function TFilterFile.PutByte (b : byte) : Boolean;
begin
if (FBytesInBuff = FBufferSize) then begin
if (not WriteBuffer) then begin
Result := False;
Exit;
end
else begin
FBytesInBuff := 0;
end;
end;
FBuffer^[FBytesInBuff] := b;
Inc (FBytesInBuff);
Result := True;
end;
{ Eof - возвращает True, если был
достигнут конец входного файла }
function TFilterFile.Eof : Boolean;
begin
Result := (FBuffIndx >= FBytesInBuff);
if Result then begin
try
Result := System.Eof (F);
except
Result := True;
end;
end;
end;
end.
Поскольку класс TFilterFile почти
все делает сам, использовать его вместо стандартного текстового файла ввода/вывода
оказывается очень просто. Тем не менее скорость работы меняется прямо на
глазах. Новая процедура DoFilter из листинга 1.9 использует класс
TFilterFile для выполнения файловых операций. Получившаяся программа
работает намного быстрее первоначальной версии. А самое приятное
заключается в том, что прочесть или понять ее оказывается ничуть не сложнее,
чем предыдущий, медленный вариант.
Листинг 1.9. Использование класса
TFilterFile вместо
стандартного файлового ввода/вывода
{
FILTMAIN.PAS - основной рабочий модуль программы Filter.
Автор: Джим Мишель
Дата последней редакции: 04/05/97
}
unit filtmain;
interface
{ DoFilter выполняет всю работу }
procedure DoFilter;
implementation
uses CmdLine, FileIO;
procedure DoFilter;
const
nOptions = 2;
Options : Array [1..nOptions] of OptionRec = (
(OptionChar : "i"; Option : otFilename; Filename : ""),
(OptionChar : "o"; Option : otFilename; Filename : "")
);
BigBufferSize = 65536;
var
cRslt : Boolean;
iRec : pOptionRec;
oRec : pOptionRec;
InputFile : TFilterFile;
OutputFile : TFilterFile;
c : char;
begin
cRslt := CmdLine.ProcessCommandLine(@Options, nOptions);
if (not cRslt) then
Halt;
{ Убедимся в том, что были заданы имена
входного и выходного файлов }
iRec := CmdLine.GetOptionRec (@Options, nOptions, "i");
if (iRec^.Filename = "") then begin
WriteLn ("Error: input file expected");
Halt;
end;
oRec := CmdLine.GetOptionRec (@Options,
nOptions, "o");
if (oRec^.Filename = "") then begin
WriteLn ("Error: output file expected");
Halt;
end;
{ Создаем и открываем входной файл }
InputFile := TFilterFile.Create (iRec.Filename, BigBufferSize);
if (not InputFile.Open (fioRead)) then begin
WriteLn ("Error opening input file");
Halt;
end;
{ Создаем и открываем выходной файл }
OutputFile := TFilterFile.Create (oRec.Filename, BigBufferSize);
if (not OutputFile.Open (fioWrite)) then begin
WriteLn ("Error opening output file");
Halt;
end;
{ Обрабатываем каждый символ }
while (not InputFile.Eof) do begin
c := char (InputFile.GetByte);
c := UpCase (c);
if (not OutputFile.PutByte (byte (c)))
then begin
WriteLn ("Write error");
Halt;
end;
end;
InputFile.Close;
InputFile.Free;
OutputFile.Close;
OutputFile.Free;
end;
end.
Использование шаблона
Filter
Если вам захочется поместить фильтр в хранилище,
создайте новый подкаталог в каталоге ObjRepos и сохраните в нем
файлы FILTER.DPR, FILTMAIN.PAS, CMDLINE.PAS и FILEIO.PAS.
Затем выполните команду Projectд Add to Repository и введите необходимую
информацию. Когда вам в следующий раз придется писать фильтр, вся скучная
работа уже будет сделана заранее - возьмите шаблон, подправьте параметры
и перепрограммируйте рабочий цикл.
Критика
Вам никогда не хотелось вернуться к уже законченному
проекту и переделать его заново? Нет, дело даже не в том, что наш шаблон
фильтра чем-то плох. Просто сейчас я оглядываюсь назад и думаю о
том, что многое можно было сделать по-другому.
В целом шаблон получился вполне приличным,
и я уже воспользовался им при написании нескольких программ, от самых примитивных
до чрезвычай но полезного потокового редактора.
Пожалуй, требования к командной строке
получились излишне строгими - все параметры должны указываться с
помощью префиксов, не поддерживается работа с конфигурационными файлами.
С одной стороны, это не так уж страшно, зато программирование заметно упрощается.
С другой стороны, поддержка конфигурационных файлов была бы нелишней, и
благодаря структуре нашей программы реализовать ее не так уж сложно. Единственное,
что мне еще хотелось бы изменить (и то из чисто косметических соображений)
- использование типа ShortString для строковых параметров и имен
файлов. Вероятно,
PString или PChar будет работать более
эффективно.
С TFilterFile дело обстоит иначе.
Этот класс реализует абсолютный минимум функций, необходимых для файлового
ввода/вывода. Вероятно, вы уже заметили, что в нем отсутствует механизм
блочного чтения/записи, а также возможность произвольного доступа к файлам1.
Многие программы-фильтры используют одну или обе из этих возможностей.
Блочные операции реализуются довольно просто - следует лишь воспользоваться
нетипизиро ванным параметром var и счетчиком байтов по аналогии
со стандартными процедурами BlockRead и BlockWrite. Эти процедуры
должны выполнять блочный обмен данными между пользовательской структурой
и буфером объекта. Не забудьте реализовать чтение и запись в файл в случае
необходимости.
1Все перечисленные функции (кроме
автоматической буферизации) реализованы в файловом потоке TFileStream,
описанном в модуле Classes. -
Примеч. ред.
Для операций GetByte и PutByte
я воспользовался методами, а не свойства ми. С минимальными изменениями
в TFilterFile можно было определить два свойства:
property InByte : byte read GetByte;
property OutByte : byte write PutByte;
а заодно превратить в свойство и Eof.
Такое изменение выглядит привлека тельным в некоторых отношениях, но мне
не понравилась перспектива остаться без кода возврата при вызове функции
вывода. В итоге я решил оставить все три функции в виде методов. Кроме
того, можно было организо вать обработку исключений ввода/вывода в блоке
try/finally.
Лично меня огорчает, что байтовое значение,
возвращаемое GetByte, приходится явно преобразовывать в символьный
тип. Конечно, в класс TFilterFile можно было включить методы GetChar
и PutChar, но черт побери! Символ - это байт, и я буду обращаться
с ним, как с байтом 1. Это один из случаев, когда C ведет себя
более разумно, а Object Pascal страдает излишними ограничения ми. Редко,
но случается и такое. Наверное, в преобразовании типов нет ничего страшного,
но я стараюсь избегать их, потому что в программировании они считаются
моветоном. В сущности, вы говорите компилятору: «Да, я и сам знаю, что
нарушаю правила. Заткнись и делай, что велено». Я предпочитаю избегать
подобных ситуаций.
1Не следует только забывать,
что на смену кодировкам OEM и ANSI постепенно приходит система Unicode,
где символ - уже не байт, а слово (в Delphi - тип данных WideChar). - Примеч.
ред.
|