• Console I/O
• Console Filter Programs
• Console Command-Line Parsers
• Console File I/O
• Using Delphi‘s Object Repository
Now that DOS has been stuffed and hung on the wall of Win32 as
a minor API, how’s a hacker to do command-line text filters?
Well, the POSIX fairy waved her magic wand, and Poof! DOS
became The Console, and it’s deja vu all over again.
Windows, OS/2, the Macintosh, and other graphical user interfaces have
been the darlings of the computer press (both technical and non-technical) for
many years now. With all that attention focused on writing applications for
these GUI environments, it’s sometimes tough to remember that there is
another world out there—a world of command-line tools that perform batch
processing with a minimum of user input. These tools aren’t especially sexy,
but they’re certainly useful. Banks, for example, still process your checks,
deposits, and loan payments in batches every night. Insurance and credit card
companies also perform nightly updates, as do countless other businesses.
(And do they use fancy GUI environments? Ask your bank teller. Or guess.)
Command-line tools aren’t limited to companies running financial programs
on big iron. Windows 95 itself comes with quite a few: ATTRIB, DISKCOPY,
FORMAT, FDISK, SORT, and XCOPY among them. Even Delphi comes
with some command-line tools. A quick peek in Delphi’s BIN directory
reveals (among others) the Resource Compilers (BRC32.EXE and
BRCC32.EXE), the Pascal compiler (DCC32.EXE), and the linker
(TLINK32.EXE).
Console Applications
Windows 95 and Windows NT support console applications—programs that
have no GUI presence but instead run in what is commonly referred to as a
“DOS box.” Although these applications don’t have a window, they do have
access to the Windows API and the full 32-bit Windows address space
(including virtual memory). This is in contrast to Windows 3.1, where GUI
programs had access to Windows’ address space, and DOS programs had
access to the lower 640K.
In the past, DOS applications got around the 640K limitation through the use
of DOS extenders that supported standards like DPMI (DOS Protected Mode
Interface) and VCPI (Virtual Control Program Interface). If you had a 16-bit
extender, you had access to 16 megabytes of memory. The less common 32-bit
extenders gave you access to the full 32-bit address space, and some even
supported virtual memory. The problem with DOS extenders is that they
are—no matter how well presented—hacks. And many users simply couldn’t
get their older machines to reliably run the DOS extenders, and some DOS
extenders couldn’t run in a Windows DOS box.
A console application running under Windows 95, on the other hand, is simply
a Windows program without a window. There’s no special extender software
required, and any computer that can run Windows 95 or Windows NT will
support console applications.
So we’ve got the RAM and we’re free of the GUI. What can we do with it?
Filters
Probably the most common use of command line tools in the PC world is the
broad category of programs called “filters.” A filter can be anything from a
very simple line counter to a complex compiler (like Delphi’s Pascal
compiler), a sorting utility, or a batch update program.
All filters operate basically the same way: They’re invoked from the command
line and passed arguments that specify options, and input and output files. The
filter reads the input, applies some processing (such processing modified by
the options specified on the command line), and writes an output file.
Filters typically don’t access the mouse, and in fact, rarely accept user input. If
they do accept user input, it’s through a very simple text-oriented interface.
Output to the user is normally limited to status reports (“Working, please
wait...”), error notifications, and a final “done” message.
In this chapter, we’re going to build a relatively simple filter program with
Delphi, and construct a filter program “shell” that you can use to quickly build
other filter programs. Along the way, we’ll learn a thing or two about Delphi’s
Object Repository, reusable code, and (shudder) process-oriented
programming.
NOTE:
I find it ironic that three years ago I was explaining Windows programming
to DOS programmers, telling them how to move away from their
process-oriented mindset and move into the wide world of event-driven
Windows programming. With the advent of visual development tools like
Visual Basic and Delphi, many new programmers started with
event-oriented programming and haven’t ever written a process-oriented
command line tool. Now I’m explaining process-oriented programming to
event-oriented programmers. Plus ça change. One good thing, though:
Programmers who understand event-oriented programming have little
trouble understanding process-oriented code. The reverse, sadly, is not true.
Console Applications and Delphi
Although it’s possible to write console applications with Delphi, the
documentation is suspiciously silent on exactly how such a thing is done.
Considering the excellent demo programs that explore so many other facets of
Delphi, I found the lack of an example console application surprising.
Fortunately, creating a console application with Delphi is not very difficult,
although it would have been nice to be informed about a couple of the details.
(Trial-and-error is such an inefficient way to learn!)
The simplest console app is, of course, the “Hello, world” program. It’s not an
exciting program, but it’s usually the first program I ever write with a new tool
because it lets me learn about the tool without having to worry too much about
the program itself. And once we’ve created a simple console app with Delphi,
we can save the code in the Object Repository so it can be used as the starting
point for other console apps. Let’s get started.
Hello, Delphi
Start with a new application (File|New Application). First we need to change
some of the project options to tell Delphi that we’re creating a console app. Select
Project|Options, and on the Linker page of the Project Options dialog box, check
the “Generate console application” check box, and then click on OK to save this
change.
Since console applications don’t have a main form (or any other form for that
matter), we need to remove the Form1 that is automagically created when you
start a new application. Select File|Remove from Project, and when the Remove
From Project dialog box appears, highlight the line that contains Unit1 and
Form1, and click on the OK button. If a message box appears and asks you if you
want to save changes to Unit1, say No. You’ll be left with a Delphi screen that
has only the Object Inspector. No Form or Unit window will be shown. So where
do you write the code?
The one thing that’s left is the project source file. Select View|Project Source, and
Delphi will display a text editor window that contains the source of
PROJECT1.DPR. It’s this file that we’re going to modify in order to create our
first console application. Before you do anything else, select File|Save, and save
the project as HELLO.DPR.
Modify the project source in the editor so that it resembles Listing 1.1, save your
work, and then press F9 to compile and run the program.
Listing 1.1 The “Hello, Delphi” program
{$APPTYPE CONSOLE}
program Hello;
uses Windows;
begin
WriteLn ('Hello, Delphi!');
Write ('Press Enter...');
ReadLn;
end.
The first line in Listing 1.1 is a compiler directive that tells Delphi to create a
console application, and must be included at the top of any console application.
This line should only be included in programs—not units or libraries (DLLs). The
uses statement isn’t necessary for the program (after all, the program isn’t making
any Windows API calls), but for some reason, Delphi doesn’t like to save a
project that doesn’t have a uses statement (see again my comment about
trial-and-error learning). Windows seemed like a fairly innocuous unit to list here,
and listing a unit doesn’t mean that it’s linked in—only that Delphi will search
that unit if it can’t find an identifier.
The rest of the program is real simple. The string “Hello, Delphi!” is output to the
console (that is, the screen), and then you’re prompted to press Enter. I included a
prompt for the Enter key because, without it, Delphi just displays a console
window (“DOS box”) briefly, runs the program and displays the hello message,
and then closes the console window. The prompt lets you see that the program
actually works.
Saving a Program Template
The steps required to build a console app aren’t especially difficult, but there are a
few details to remember. Rather than building from scratch every time (and
forgetting a detail or two), let’s save our little Hello program in the Object
Repository so that we’ll have a starting point for other console apps.
Using the Windows Exploder (what we called File Mangler under Windows NT
3.51), create a subdirectory called ConsoleApp in Delphi’s Objrepos subdirectory.
If you installed Delphi using the standard options, the full path name will be:
C:\Program Files\Borland\Delphi 2.0\Objrepos\ConsoleApp
Then, select Project|Save Project as from Delphi’s main menu, and save the
project as ConsoleApp.dpr (don’t you just love long file names?) in that new
directory.
Once you’ve saved the project, add it to the repository by selecting Project|Add to
Repository, and fill in the Add to Repository dialog box Once you’ve added the project to the Repository, select File|New from Delphi’s main menu, select the Projects page of the New Items dialog box, and double-click on the “Console Application” icon. Delphi will prompt you for a
directory and create a new project that has the options set for the console application.
NOTE:
I haven’t quite decided if it’s a good idea to store your own objects in Delphi’s
Object Repository directory. Whereas it’s a handy place to put things, it’s also
asking for trouble if you ever upgrade your version of Delphi. If you upgrade,
it’s quite likely that Delphi’s Objrepos directory will be deleted—along with all
of your cool objects. You’ll have to back up your objects before you upgrade.
You can, if you’d rather, create your own Repository directory that’s not a child
of Delphi’s main directory. Either way, you’ll have to again add the projects to
Delphi’s repository after you upgrade, but with a separate directory, you won’t
run the risk of inadvertantly deleting your projects’ source code.
Console Input and Output
When a console application is started, the Input and Output standard text files
are automatically associated with the console window. As a result, the
ReadLn and WriteLn procedures work as expected, as do Eof, Eoln, Read,
Write, and the other Text file I/O functions.
There are a number of console-specific I/O functions that can come in handy
from time to time. Unfortunately, these console-specific functions are defined
in the Windows console interface and there’s no convenient Delphi component
wrapper to insulate us from the gory details. (Now there’s a good shareware
project for an enterprising programmer: a Delphi class that encapsulates the
Windows console interface.) The Windows console interface is a chapter in
itself, so I’m going to conveniently ignore it here. If you’re interested in
learning more about PeekConsoleInput, WriteConsole, and the rest of the
console API functions, check out the “Console Functions” entry in Delphi’s
online help.
Because we don’t have space here to discuss the Console API, we’re going to
limit our console input and output to the standard Text file I/O routines. Don’t
take this the wrong way. The Console API functions are useful for some
applications—just not for the kinds of applications that you’ll typically write
as console apps. Yeah, I know, it’s confusing. It turns out that the Console API
is much more useful for GUI programs that want to control a console window
than it is for straight console applications controlling themselves.
Console apps aren’t limited to a boring old text mode interface. Since you’ve
got access to the full Windows API, you can display message and dialog
boxes, control other windows, and even create another console from within
your application.
Filter Programs in Delphi
Now that we know how to create a console application, let’s put that
knowledge to use. The rest of this chapter is concerned with writing filter
programs as console apps. After an overview of how filter programs work,
we’re going to discuss processing the command line and efficient file
operations. We’ll be cutting a wide swath through Delphi’s standard run-time
library, and won’t have time to discuss every function in detail. Remember that
online help is your friend—use it early and often.
Your Basic Filter Program
As I mentioned at the beginning of this chapter, filter programs typically
accept a command line that specifies options and input/output filenames,
crunch the input as specified by the options, and produce the output file.
Given that general description, there’s lots of room for improvisation. A line
counter program, for example, could accept multiple input file names
(including wildcards), and could have options that tell it to report not only the
number of text lines in a file, but also the number of words and characters, and
possibly a frequency distribution of words and characters. In a more involved
program, the output can be a simple transformation of a single input file, a
single file that combines multiple input files, or many different files created
from a single input file.
Despite the differences in complexity, filters share a large amount of common
functionality. They all process the command line, read input files, and write
output files. Only the intermediate processing step changes significantly from
one program to another. Because of this commonality, it’s possible to build a
group of functions that provide the common functionality and allows you to
quickly build a custom filter program by simply defining how the command
line is to be parsed and writing the code for the “processing” step. The input,
output, and command line parsing portions are all there. Kind of a dehydrated
filter program—just add processing.
Processing the Command Line
Command line processing sounds so simple. Given a text string that represents the
command line, we want to parse out the file names and options, and set the
program’s variables accordingly. I’m continually amazed at how difficult such a
simple-sounding thing can be. Fortunately, Object Pascal has two standard
functions, ParamCount and ParamStr, that make things a bit easier.
ParamCount simply returns a count of the parameters on the command line. So if
your command line is “MyFilter file1.txt file2.txt”, ParamCount will return 2.
The program name itself isn’t counted as a parameter by this function.
ParamStr accepts an integer and returns a string that contains the command line
argument that corresponds to that integer. For example, given the above command
line, this statement
WriteLn (ParamStr (1));
will output ‘file1.txt’ (without the quotes).
If you pass 0 to ParamStr, the returned string will contain the full path and file
name of the program that’s currently being executed.
The Params program shown in Listing 1.2 illustrates the use of ParamCount and
ParamStr. To create this program, select File|New from Delphi’s main menu,
select the Console Application item from the Projects page of the New Items
dialog box, and then tell Delphi where to put your new application. Be sure to
save the project as Params.dpr before you modify it.
Listing 1.2 The Params program
{$APPTYPE CONSOLE}
{
Params -- a simple exploration of the ParamCount and
ParamStr functions.
}
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.
If you want to test the program from within Delphi, you need to select
Run|Parameters from Delphi’s main menu and enter the command line that you
want to have passed to the program. For the above example, you’d enter the string
“file1.txt file2.txt” (without the quotes) in the Run parameters dialog box.
Simple, no? Unfortunately, not so simple. Back in the days of DOS and Windows
3.1, things really were simple. But then along came long file names with
embedded spaces. Now we have a problem. You see, ParamCount and
ParamStr assume that command line arguments are separated by spaces. This
works fine as long as your files don’t have embedded spaces, but try this
command line:
params c:\program files\borland\delphi 2.0\readme.txt
ParamCount returns 3, and the individual parameters it reports are
c:\program
files\borland\delphi
2.0\readme.txt
which is clearly not what we intended! (Okay, so maybe long file names aren’t all
peaches and cream—beer and skittles if you’re British. Warm beer.)
I won’t go into all the possible solutions to this problem. If you want a full
discussion of this problem and the possible solutions (none of which are
satisfactory, by the way—thank you Microsoft), get a copy of Lou Grinzo’s Zen
of Windows 95 Programming, also published by Coriolis Group Books. The book
is ostensibly about C and C++ programming for Windows 95, but there’s a wealth
of good information in there for all programmers, especially in the way of writing
bug-free programs. This book is among the top three programming books I’ve
ever read, along with Writing Solid Code and Debugging the Development
Process, both written by Steve Maguire and published by Microsoft Press.
The only workable (although not satisfactory) solution to the embedded spaces
problem is to require that file names containing embedded spaces be surrounded
by quotation marks. So our sample command line becomes:
params "c:\program files\borland\delphi 2.0\readme.txt"
I guess you could require that your users always pass the short version of the file
name, but I suspect they’d be more upset having to enter this
params c:\progra<1\borland\delphi<1.0\readme.txt
than they would be about the quotation marks.
Command Line Options
Most (definitely not all) command line programs get their parameters via the
command line. Sometimes you’ll see programs that receive their parameters from
environment variables or configuration files, and some are hybrids that can accept
parameters from the command line or from a configuration file whose name is
specified on the command line. Since we don’t want to get too bogged down in
parameter processing, we’re going to ignore the configuration files and
environment variables, and concentrate solely on command line parameters.
You’ve probably used a command line tool (like DIR) that accepts options
prefaced by a slash (/). For example, if you want a listing of files in the current
directory and all of its subdirectories, you’d enter the command: DIR /S. Many
programs also accept command line parameters prefaced by the dash (or minus
sign, -). Both are common, and many programs will accept either.
File names, on the other hand, are specified in many different ways, depending on
the tool. COPY, for example, lets you specify the name of the input and output
files without prefacing them with option characters. So COPY FILE1 FILE2
copies FILE1 to FILE2. Borland’s MAKE program, on the other hand, requires
that you preface the input file name with the -f parameter. So, to process
BUILD.MAK, you’d enter this command: MAKE -fbuild.mak.
The way MAKE processes command lines is easier, because everything is an
option. Every option is separated from the others by at lease one space, and file
names are handled just like other options—there aren’t any special cases. This is
the model we’re going to use for our filter programs.
In general, there are four kinds of command line options: switches, numbers,
strings, and file names. Switches simply turn an option on or off. A text filter, for
example, might have a switch option to convert all lower-case characters to
upper-case. Numbers can be integers or floating point, and can be specified in any
number of ways: decimal and hexadecimal being the most common. Strings and
file names are similar, although file names are often validated to ensure that
they’re properly formed.
A Reusable Command Line Parser
If there’s anything I dislike about programming, it’s slogging through dozens (or hundreds) of lines of code
to do something for the tenth (or hundredth) time. Command line parsing is like that: Every filter program
has to process the command line, and command line parsing is boring code after you’ve written it once or
twice. So I keep trying to come up with a generalized command line parser that will, with a minimum of
effort on my part, parse a command line and fill in my program’s options structure. That way, I’ll have more
time to spend on the filter (the real problem, after all) rather than on the command line parser.
A generalized command line parser is not an easy piece of code to write, and even a minimal one can be a bit
involved. The one we’ll develop here is minimal, but functional for many applications.
The basic idea is to define the valid option characters, the type of each option, and the default value for each
option. The structure that contains this information is passed to the command line parser, which chews up the
command line and fills in the values for the individual options that it finds. If it finds an error (an invalid
option or a number where it expected a switch), it spits out an error message, stops processing, and returns an
error status to the calling function. Simple, right? Ahhh...but not so easy.
An individual options record takes the format of the OptionsRec structure shown in Listing 1.3. This listing
contains the full source of the CmdLine unit. You should create a new file in your editor, and enter and save
this code as CMDLINE.PAS.
Listing 1.3 The CmdLine unit
{
CMDLINE.PAS -- Command line parameters parsing
}
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 -- return a pointer to the options record in the
passed Options array that corresponds to the specified option
character. Returns Nil if the option character is not in
the passed Options array.
}
function GetOptionRec
(
Options : pOptionsArray;
nOptions : Integer;
OptionChar : char
) : pOptionRec;
{
ProcessCommandLine -- process the command line according to the
parameters list passed in the Options array. Returns True if
successful, or False if an error occurred in processing.
}
function ProcessCommandLine
(
Options : pOptionsArray;
nOptions : Integer
) : Boolean;
implementation
uses SysUtils;
{
GetOptionRec -- return a pointer to the options record in the
passed Options array that corresponds to the specified option
character. Returns Nil if the option character is not in
the passed Options array.
}
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
Extract the on/off state for a parameter. If the passed Param
is a blank string, it is assumed to be On (+). Otherwise the
routine expects the string to start with + or -, and sets the
OnOff variable accordingly.
}
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
Extract an integer from the passed command line parameter.
}
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
Copy the passed string to the Option variable. No error checking
is performed, and a blank string is considered a valid parameter.
}
function ProcessString
(
Param : String;
var Option : ShortString
) : Boolean;
begin
Option := Param;
Result := True;
end;
{
ProcessFilename
Extract a file name from the passed command line parameter.
Currently, this function just calls ProcessString to copy the
string parameter to the Filename. It could, in the future,
check to see if the string represents a valid file name, or it
could be used to expand a short filename to a full path/file.
}
function ProcessFilename
(
Param : String;
var Filename : ShortString
) : Boolean;
begin
Result := ProcessString (Param, Filename);
end;
{
CheckParam
Check the passed Param, representing one command-line argument, against
the list of options. If the option character is valid, then process
the option based on its type (Boolean, Integer, String, or Filename).
Returns True if option processed and stored correctly, False otherwise.
}
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
Given a list of option characters and parameter types, check each
command line argument against the list and set the values in the
options structure accordingly.
Returns True if all parameters processed and stored successfully.
}
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.
You like this article ? Stay tuned and visit us often because there will be more !
read more →