|
|
|
WEB-БИБЛИОТЕКА |
|
Для просмотра сайта
рекомендуется :
-Разрешение : 800*600 -Броузер : Internet Explorer -Для более эффективного просмотра нажмите F11. |
32-разрядные консольные приложения
Высушенное чучело DOS красуется ныне на стенке Win32 в качестве второстепенного API. Как же теперь бедному хакеру создать текстовый фильтр, запускаемый из командной строки? Добрая фея POSIX взмахивает волшебной палочкой… Дзынь! DOS на глазах превращает ся в консольное приложение, вызывая мучительное ощущение deja vu. В течение многих лет Windows, OS/2, Macintosh и другие графические пользовательские интерфейсы (GUI) оставались излюбленной темой компьютерной прессы. Когда основное внимание уделяется разработке приложений для GUI, бывает трудно вспомнить о том, что существует и другой мир - мир средств командной строки, которые выполняют пакетные вычисления с минималь ным вводом информации от пользователя. Пусть такие программы выглядят не слишком эффектно - несомненно, они приносят немалую пользу. Скажем, банки обрабатывают сведения о ваших чеках, вкладах и ссудных платежах ночью, в пакетном режиме. Страховые и кредитные компании, а вместе с ними и другие бесчисленные учреждения тоже обновляют информацию по ночам. Нужны ли им для этого красивые среды GUI? Спросите своего кассира в банке. Или попробуйте угадать сами. Возможности средств командной строки отнюдь не ограничиваются финансовыми расчетами на «крутом железе». Несколько таких программ входит в комплект Windows 95, среди них - ATTRIB, DISKCOPY, FORMAT, FDISK, SORT и XCOPY. Они присутствуют даже в Delphi - при самом поверхностном просмотре каталога BIN там можно найти компиляторы ресурсов (BRC32.EXE и BRCC32.EXE), компилятор языка Паскаль (DCC32.EXE) и другие программы. Консольные приложения В прошлом 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. Возникает вопрос - что делать дальше? Фильтры Все фильтры построены на одном принципе: они вызываются из командной строки и получают аргументы, в которых задаются параметры их работы, а также имена входных и выходных файлов. Фильтр читает входные данные, выполняет некоторые вычисления (зависящие от параметров, указанных в командной строке) и записывает результат в выходной файл. Фильтры обычно не работают с мышью и вообще очень редко взаимодействуют с пользователем. Если же фильтр все-таки получает информацию от пользователя, то для этого применяется простейший текстовый интерфейс. Вывод, как правило, ограничивается информацией о ходе процесса («Working, please wait…»), сообщениями об ошибках и завершающим сообщением «Done». В этой главе мы напишем на Delphi относительно простую программу -фильтр, построив при этом «каркас», на основе которого можно будет легко создавать другие фильтры. Попутно мы узнаем кое-что о хранилище объектов Delphi, многократном использовании кода и (содрогнитесь от ужаса) процессно-ориентированном программировании. Замечание Ирония судьбы - всего три года назад я преподавал программирование для Windows DOS-программистам и рассказывал им о том, как отказаться от традиционного процессно-ориен тированного мышления и войти в широкий мир управляемых событиями Windows-программ. С появлением визуальных средств разработки - таких как Visual Basic и Delphi - многие новички сразу начинают с событийного программирования и даже не умеют писать процессно-ориентированные средства командной строки. А теперь я рассказываю вам о том, как от событийного программирования вернуться к процессно-ориентированному. Plus зa change. Единственный «плюс» заключается в том, что программист, привыкший работать с событиями, без особых трудностей поймет процессно-ориентированный код. Обратное, к сожалению, неверно. Консольные приложения на Delphi Простейшее консольное приложение - это, конечно же, программа «Hello World». Выглядит она не особо эффектно, но обычно я начинаю освоение всех новых программных средств именно с нее. Дело в том, что с помощью «Hello World» можно кое-что узнать о новой среде, не заботясь о содержании программы. После того как мы напишем на Delphi простейшее консольное приложение, его код можно будет отправить в хранилище объектов и пользоваться им как отправной точкой для создания других аналогичных проектов. Hello, Delphi Поскольку у консольного приложения нет главной формы (и, если уж на то пошло, вообще никаких форм), необходимо удалить форму 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 { uses Windows; begin Строка {$APPTYPE CONSOLE} в листинге 1.1 является директивой компилято ра и сообщает Delphi о том, что создаваемое приложение является консольным. Она должна присутствовать в начале любого консольного приложения. Эта директива включается только в программы - она не нужна в модулях или библиотеках динамической компоновки (DLL). Ключевое слово uses нашей программе, вообще говоря, не нужно (мы здесь не обращаемся к функциям Windows API), но по какой-то загадочной причине Delphi не любит сохранять проекты без секции uses (см. мое замечание о методе проб и ошибок). Включение модуля Windows не принесет никакого вреда и говорит вовсе не о том, что модуль подключается к программе, а лишь о том, что Delphi просмотрит его, если не сможет найти какой-нибудь идентификатор в текущем модуле. Оставшаяся часть программы проста до очевидного. Строка «Hello, Delphi» выводится на консоль (то есть на экран), после чего вам будет предложено нажать Enter. Я включил сюда ожидание ввода лишь потому, что без него Delphi на долю секунды выведет окно консоли (сеанса DOS), запустит программу и сразу же закроет окно. Ожидание нажатия Enter позволяет убедиться в том, что программа действительно работает. Сохранение шаблона программы С помощью 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. Замечание Я так и не решил, стоит ли держать свои объекты непосредственно в каталогах хранилища Delphi. Это довольно удобно, но любое обновление версии Delphi может обернуться неприятностями. Скорее всего, при обновлении каталог Objrepos будет удален - вместе со всеми замечательными объектами, которые в нем находятся. Вам придется вручную сохранять их перед каждым обновлением. Существует и другой вариант - создать собственный каталог-хранилище, не принадлежа щий основному каталогу Delphi. В любом случае при обновлении Delphi вам придется заново включать объекты в хранилище, но отдельный каталог по крайней мере защитит ваши проекты от случайного удаления. Консольный ввод/вывод Существует целый ряд консольных функций ввода/вывода, которые время от времени оказываются полезными. К сожалению, эти функции определены в консольном интерфейсе Windows, и в Delphi не существует никакой удобной оболочки, которая скрывала бы от нас все отвратительные техниче ские подробности (кстати, напрашивается отличный shareware-проект для талантливого программиста - класс Delphi, инкапсулирующий консольный интерфейс Windows). Консольный интерфейс Windows сам по себе требует отдельной главы, поэтому сейчас я обойду его деликатным молчанием. Если вы захотите побольше узнать о PeekConsoleInput, WriteConsole и других функциях консольного API, обратитесь к разделу Console Reference файла WIN32.HLP из подкаталога Help Delphi. Программа установки не создает ссылку на этот файл, так что вам придется самостоятельно найти и загрузить его. Из-за недостатка места для полноценного обсуждения консольного API работа с консолью в нашем приложении будет ограничена стандартными функциями файлового ввода/вывода. Поймите меня правильно - функции консольного API могут принести пользу во многих приложениях, но только не в тех, которые обычно пишутся как консольные. Да, я знаю, что это звучит довольно странно, но, похоже, консольный API больше подходит для GUI-программ, управляющих консольными окнами, а не для обычных консольных приложений, которые работают сами по себе. Возможности консольных приложений не ограничиваются унылым текстовым интерфейсом. Поскольку у вас имеется полный доступ к Windows API, вы можете отображать окна сообщений и диалоговые окна, управлять работой других окон и даже создавать другие консольные окна из своего приложения. Программа-фильтр на Delphi Базовая программа-фильтр Столь общее описание оставляет более чем достаточно возможностей для импровизации. Например, программа для подсчета строк может получать имена сразу нескольких файлов (в том числе и файловые маски), а при указании некоторого параметра - считать не только текстовые строки, но также слова и символы или даже выдавать распределение слов и символов по относительной частоте. В более сложной программе результат работы может представлять собой отдельный файл, полученный преобразованием одного или нескольких входных файлов, или сразу несколько файлов, полученных в результате обработки одного входного файла. Несмотря на все различия в сложности, фильтры обладают рядом общих функций. Все они обрабатывают содержимое командной строки, читают входные файлы и записывают выходные. Разные программы существенно отличаются друг от друга лишь промежуточной стадией обработки. Благодаря этой общности можно создать группу функций, которые реализуют основные задачи фильтров и позволяют быстро создавать нестандартные фильтры, для чего потребуется лишь указать синтаксис командной строки и написать код для стадии «обработки». Ввод, вывод, анализ командной строки - все это уже присутствует. Программа-фильтр хранится в виде концентрата, остается лишь добавить воду... то есть обработку. Обработка командной строки 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 { Автор: Джим Мишель uses Windows; Var begin for i := 1 to ParamCount do Write ("Press Enter..."); Не правда ли, просто? К сожалению, не совсем. В старое доброе время DOS и Windows 3.1 все было действительно просто. Но потом появились длинные имена файлов, которые к тому же могли содержать пробелы. Возникает проблема. Видите ли, функции ParamCount и ParamStr предполагают, что аргументы командной строки разделяются пробелами. Все идет замечательно, пока имена файлов не содержат пробелов, но попробуйте-ка ввести такую командную строку: params c:\program files\borland\delphi 3\readme.txt Функция ParamCount возвращает 3, а параметры с ее точки зрения выглядят так: c:\program Получается совсем не то, что мы ожидали увидеть! (Пожалуй, длинные имена файлов не всегда хороши. Иногда они вызывают сплошные огорчения.) Я не стану углубляться в обсуждение этой темы. Если вам захочется побольше узнать о проблеме и ее возможных решениях (ни одно из которых, кстати говоря, нельзя признать удовлетворительным - спасибо тебе, 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 - unit cmdline; interface type pOptionRec = ^OptionRec; pOptionsArray = ^OptionsArray; { { implementation uses SysUtils; { { Определяет состояние параметра-переключателя (вкл/выкл). if (Length (Param) = 0) then begin case Param[1] of else begin end; { Извлекает целое число из переданного параметра Result := True; { Копирует переданную строку в переменную Option. { { Проверяет, принадлежит ли аргумент командной { По заданному списку префиксов и типов Возвращает True, если все параметры были var begin for ParamNo := 1 to ParamCount do begin end. Запись 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 Проект Filter предназначен для проверки модуля CmdLine, а также модуля файлового ввода/вывода, которым мы займемся далее. После завершения работы над модулями их окончательные версии будут помещены в хранили ще, и у нас появится шаблон для создания фильтров. Для проверки модуля CmdLine нам понадобится массив с информацией о параметрах и фрагмент кода, в котором вызывается ProcessCommandLine. Тестовая программа (файл FILTER.DPR) приведена в листинге 1.4. Листинг 1.4. Программа FILTER.DPR для тестирования модуля CmdLine { Автор: Джим Мишель uses Windows, CmdLine; const Options : Array [1..nOptions] of OptionRec = ( var Rec := CmdLine.GetOptionsRec (@Options, nOptions, "i"); Write("Press Enter..."); После инициализации таблицы параметров (это происходит в секции const) вызывается функция ProcessCommandLine, которая читает аргументы командной строки и сохраняет значения параметров в таблице. Затем программа выводит результат, возвращенный функцией ProcessCommandLine, вместе со значени ями всех параметров. Попробуйте задавать этой программе различные командные строки. Не ограничивайтесь правильными строками и обязательно введите несколько неправильных, чтобы убедиться в корректной обработке ошибок. Могу предложить несколько вариантов: -iInFile.txt -oOutFile.txt -n995 -d{правильная строка} -n8.94 {Error: integer expected} Обобщенный анализатор командных строк, содержащийся в модуле CmdLine, позволяет очень легко получить параметры нашей программы. Достаточно заполнить таблицу и передать ее функции ProcessCommandLine, которая и выполнит всю необходимую работу. Все, что от вас требуется, - проследить за тем, чтобы все необходимые параметры были заданы, и присвоить значения внутренним переменным программы в соответствии с указанными параметрами. Поверьте, это намного проще, чем писать отдельный анализатор для каждой программы. Несколько слов о структуре программы Самая главная причина заключается в том, что 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} uses begin { Автор: Джим Мишель interface { DoFilter выполняет всю работу } implementation uses CmdLine; procedure DoFilter; Options : Array [1..nOptions] of OptionRec = ( var Rec := CmdLine.GetOptionsRec (@Options, nOptions, "i"); Write("Press Enter..."); end. Файловые операции чтения/записи Листинг 1.7. Перевод символов в верхний регистр procedure DoFilter; const Options : Array [1..nOptions] of OptionRec = ( var begin { Убедимся в том, что были заданы имена входного oRec := CmdLine.GetOptionRec (@Options, { Открываем входной файл - без проверки ошибок} { Создаем выходной файл - без проверки ошибок} { Читаем и преобразуем каждый символ } Close (InputFile); программ, которым может понадобиться работать и с двоичными файлами. Да и скорость работы не мешало бы повысить. Поэтому необходимо найти более универсальный и быстрый способ чтения символов (или байтов) из файла. Нам придется самостоятельно организовать буферизацию; программа при этом усложняется, но результат стоит затраченных усилий. Класс TFilterFile из листинга 1.8 предназначен для организации быстрых побайтовых операций с файлами в программах-фильтрах. Он инкапсулирует все детали буферизации и по возможности избавляет программиста от необходимости помнить о многочисленных житейских проблемах работы с файлами (вам остается лишь вызвать Open и Close). Листинг 1.8. Реализация класса TFilterFile из файла FILEIO.PAS { |