Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Design

Structured Programming


AUG88: STRUCTURED PROGRAMMING

One of the first things a new user to DOS learns is how to use the DIR command. And as soon as the Wonder Of It All starts to lose its magical shimmer, he or she begins to chafe against its inadequacies: "One of these days, I'll write something better." Well, after five years of such grumbling, I finally got around to writing a better program, which I will share with you in this month's column. Along the way, you'll see the way to do some system-level programming with Turbo Pascal 4.0.

There are many complaints about DIR. Here are my favorites:

There are a number of everyday situations in which DIR doesn't quite do the job. For example, say you want to copy a subdirectory to a floppy disk. But how many disks do you need? With DIR you have to use a calculator to add up the space. That's dumb: that's the sort of work computers are meant to do. Besides, a file almost always requires more space than its number of bytes, so even a manual computation is a guess. The same principle holds true if you want to copy all your .PAS files from a directory.

Another example of the problems with DIR occurs when you're trying to get rid of a directory. You must first delete all the files, then go to the next higher level, and remove the directory with RmDir. However, DEL doesn't delete hidden, system, or read-only files, and DIR doesn't even let you know that the first two types of files exist. Thus, RmDir refuses to cooperate, and you haven't a clue as to the reason. You'd know the reason if you could see all the files and their attributes.

And finally, it's good to know how full your disk is, rather than how many bytes are free. You can set an arbitrary threshold, such as 90 percent, at which point you need to consider cleaning house.

Enter the improved DIR. I call it SUB because it's chiefly a subdirectory listing tool. SUB accepts either no command-line argument (meaning "list all files in the current directory"), or DIR-style wildcards. Table 1, on page 128 presents the commandline parameters for SUB.

Table 1: Command-line parameters for SUB.



   .#                       Same as *.#
   \DIR                     Everything in another directory
   \DIR\.#                  Specified flies in another directory
   \DIR\#.#                 Ditto
   \DIR\SUBDIR\...          Same for longer paths
   \DIR\SUBDIR\...\#
   \DIR\SUBDIR\...\#.#
   SUBDIR                   Everything in a lower subdirectory
   SUBDIR\.#                Same as above, lower subdirectories
   SUBDIR\#.#
   SUBDIR\#.#, and so on.

     The symbol # denotes any combination of wildcards and literal
     characters forming a filename mask. For example, either SUB
     \DOS\COM or SUB\DOS\*.COM lists all COM flies in the DOS directory.
     Similarly, SUB\LOTUS\AC*.WK? lists all .WKS and .WK1 flies
     beginning with the letters AC in the \LOTUS directory
</PRE>
<P>

<P>

So far SUB should seem very familiar to you, because these arguments are exactly the same ones that DIR expects. The difference is not in the input-the difference is in the output. <a href="8808h.htm#0186_0005">Figure 1</A>, page 128, shows the results of the command SUB \ .COM on my hard disk. The command lists the five .COM files in the root directory. The display is quite different from what you get by using the equivalent DIR command. DIR would not even show the first two files, which have the attributes System and Hidden. Furthermore, DIR does not reveal that OPTIMIZE.COM is a read-only file, which SUB does.
<P>

<h4><a name="0186_0005"><a name="0186_0005"><B>Figure 1:</B> Example SUB output for a root directory</h4>
<P>

<pre>
Directory for *.COM in\:

Name              Date          Time       Attrib      Bytes      Size

IBMBIO.COM        2-30-85       12:00P     ...SH       16369      16K
IBMDOS.COM        12-30-85      12:00P     ...SH       28477      28K
COMMAND.COM       12-30-85      12:00P     A....       23791      24K
OPTIMIZE.COM      10-14-87      06:55P     A....R      1968       2K
QUICKBUF2.COM     12-1887       10:02A     A....       15440      16K

5 files, 88045 bytes, 86K space
13039616 Bytes free out of 33419264 total (39.02% utilization)

Perhaps the most important difference between the SUB output and the typical DIR output is in the last two lines. They show that the five files consist of a total of 86,045 bytes in 86K of disk space, and that the disk is 39.02 percent occupied.

The total of 86,045 bytes in 86K is unusual. A disk is divided into fixedsize allocation clusters. The number of kilobytes of occupied disk is ordinarily much higher than the number of bytes. For example, my hard disk's root directory contains 10 .BAT files with a total of 557 bytes, but the files occupy 20K.

What is the reason for this difference? The minimum space given to a file of one byte or greater is one allocation cluster. (Files of 0 bytes and volume labels, which are purely directory entries, claim no disk space.) If a file consists of one byte to 2048 bytes, it claims a 2K allocation cluster. (I'll discuss cluster sizes later in this column.) A file of 2049 bytes is 1 byte greater than the cluster size, so the file requires two clusters, or 4K. In 10 files, 557 bytes is an average of 55.7 bytes per file, yet each file takes up the minimal cluster of 2K. This size requirement is the reason that the 10 small .BAT files in my root directory use up 20K.

All this information is important in determining how many floppy disks you need to back up a set of files. Suppose you have 180 files of 1 byte each. The total data space is 180 bytes, but the total file space is 360K, or another floppy disk. This difference between data space and file space is the reason for wanting to display not only total bytes, but total kilobytes used.

The top of the SUB output contains the line

Directory for <mask> in <path>:

which indicates what is being listed. The next line gives labels for each column. The next possible 21 lines show actual directory entries satisfying <mask> in <path>. If there are less than 21 entries, as in the case of Figure 1, you will see the two summarization lines. If there are 21 entries or not, SUB skips a line and displays the prompt

 -MORE-

You simply press a key to advance to the next panel.

As with DIR, you can use SUB to examine the directories of disks other than the default. For example, to see all the files in the root directory of the diskette in drive B, type

SUB B:

The utility "signs on" to drive B, does its thing, and then returns at the DOS prompt level to the originating drive and directory. The same process occurs when you list on the default drive a directory that is other than the current one.

Now look at the flow of the overall SUB.PAS program shown in Listing One. As with all structured programs, you can understand the logic most quickly if you work down from the highest level: the body of the main program.

Because of the many forms that the command-line argument can have, the program makes up to four attempts to process the argument using different interpretations. A successful attempt sets the Boolean variable thru to TRUE, and bypasses further attempts. If no effort is successful, thru is still FALSE at the end, so the program prints the message

PATH NOT FOUND

and quits.

Each attempt differs from the others in its set-up. The reason for this difference is to accommodate the possible interpretations of the command-line argument. The middle two attempts try to change directories using the ChDir procedure, which is similar to the DOS command. Turbo's ChDfr generates a runtime error when the path is not found; to get around this, the program uses the {$I-} pragma to disable run-time error checking, and then detects whether the procedure succeeded by checking Turbo's IOResult variable. A nonzero result indicates failure.

The third attempt calls the local procedure Separate. This procedure uses substring manipulation to break an argument such as \DOS\*.COM into its path and its file mask components (\DOS and *.COM, respectively).

All of the attempts call a procedure named ListFiles. It contains nested procedures for display control. The most interesting feature of ListFiles is, in fact, the basis for obtaining directory entries from within a program: the Turbo procedures FindFirst and FindNext. They're syntactic sugar coatings for DOS functions 4Eh and 4Fh.

A call to FindFirst loads file and attribute masks into a data structure of type SearchRec (defined in Turbo's DOS unit), and then initiates a search of the current directory for a file that matches the masks. If successful, the system global variable DosError is set to 0 and the SearchRec structure contains information about the file. Subsequent searches for other files matching the same masks are made with calls to FindNext. You can keep calling FindNext to get successive files, until DosError changes to nonzero. This change indicates that no files remain that satisfy the mask in the directory.

Let's look at the WriteFile Info procedure, which ListFiles calls for each hit made by FindFirst/FindNext. We'll discuss file attributes a little later.

WriteFile Info displays information contained in the SearchRec structure. Three nested functions do most of the work for WriteFile Info.

The SearchRec Time field is a zoned 32-bit integer that the TimeStamp function converts into a formatted string. This string shows the date and time of the file's most recent update: a lot of string-fiddling, but not especially opaque.

The SizeInK function rounds the number of bytes in the file up to the next-higher multiple of the disk's allocation cluster size. Sizeink then converts the number into kilobytes by dividing by 1024. The allocation cluster size is in the variable BlockSize, which is loaded by using a DOS call in the AllocInfo function elsewhere in the program. This size varies according to the media; for many disks, the size is 2K, but it can range from 512 bytes to 4K. [For more details on cluster sizes, check Ray Duncan's Advanced MSDOS.--ed.]

Now let's turn to file attributes. Every DOS directory entry contains a field called the attribute byte. The field is a catch-all for storing status, access, and visibility information. The low six bits are all significant and are listed in Table 2 (on page 128).

The archive bit is sometimes called the "dirty bit." This bit goes to 1 when the file is updated. Programs such as FASTBACK and the DOS BACKUP utility reset this bit for each file copied, and by checking it during subsequent runs, they determine whether or not the file needs to be backed up again.

In the DOS unit, Turbo furnishes named constants for these bits: ReadOnly, Sysfile, and so forth. The Attribs function in WriteFileInfo uses these constants to test bits and to build a displayable string that shows the file's attributes. Turbo also furnishes the constant AnyFile, which has all six low-order bits turned on.

This constant serves as the attribute mask for the invocation of FindFirst/FindNext: it tells them to select any file satisfying the name mask, regardless of its attributes.

And that's how SUB does it.

I find this SUB command much more useful than DIR, and you probably will, too. Place SUB in your \DOS directory or somewhere along the chain established by the PATH command. After you do this, you can execute it no matter where you are on the hard disk.

What's In a Directory Entry?

Turbo Pascal's FindFirst and FindNext procedures extract information from directory entries, which are the basic housekeeping structures for disk management. A DOS disk has two kinds of directories; the root is a fixed-size area on the first few tracks of the disk, while a subdirectory is a special kind of variable-sized file that contains control information about other files. Both a root directory and a subdirectory consist of 32-byte data structures.

Expressed in Pascal notation, the format of a directory entry is:

   TYPE DirEnt = RECORD
       Filename : ARRAY [0..7] OF CHAR;
       Extension : ARRAY [0..2] OF CHAR;
       Attribute : BYTE;
       Reserved : ARRAY [0..9] OF BYTE;
       Timestamp, Datestamp : WORD;
       FATentry : WORD;
       Filesize : INTEGER;
   END;

The FAT entry points to the location with the File Allocation Table where the allocation chain begins. FAT entries map one-for-one to allocation clusters.

Consequently, this is all the information DOS needs to know about any given file. A program such as SUB doesn't need all this information; the reserved area and the FAT entry are of no practical value to most applications. For this reason, FindFirst and FindNext copy the other fields to the SearchRec structure. -- K.P.

Table 2: Lower six bits of the attribute byte of a directory entry

     Bit       Means
     0         Read-only
     1         Hidden
     2         System
     3         Volume label
     4         Directory
     5         Archive

_STRUCTURED PROGRAMMING_ by Kent Porter

[LISTING ONE]

<a name="0186_0009">

PROGRAM sub;

{ Enhanced version of DIR command }
{ Turbo Pascal 4.0 }
{ K. Porter, DDJ, August 88 }

USES dos, crt;

TYPE  maskType = STRING [12];
      pathType = STRING [80];

VAR   oldDir, searchPath : pathType;
      fileMask           : maskType;
      BlockSize, Nfiles  : INTEGER;
      TotalBytes, TotalK : LONGINT;
      Thru               : BOOLEAN;

{ ------------------------------------------------------------- }

PROCEDURE WriteFileInfo (VAR files : SearchRec);

     { List filename, date, time, etc. }

VAR   KBytes : LONGINT;

  FUNCTION TimeStamp : STRING;

     { Return file date/time by unpacking time field }

  VAR  stamp             : DateTime;
       StampStr, WorkStr : STRING [20];
       ap                : CHAR;

  BEGIN
    UnpackTime (files.time, stamp);

    Str (stamp.month, StampStr);        { Format year as string }
    IF stamp.month < 10 THEN StampStr := '0' + StampStr;
    Str (stamp.day, WorkStr);
    IF stamp.day < 10 THEN
      StampStr := StampStr + '-0' + WorkStr
    ELSE
      StampStr := StampStr + '-'  + WorkStr;
    Str (stamp.year - 1900, WorkStr);
    StampStr := StampStr + '-' + WorkStr + '    ';

    IF stamp.hour > 11 THEN
      ap := 'p'
    ELSE
      ap := 'a';
    IF stamp.hour > 12 THEN
      stamp.hour := stamp.hour - 12;
    Str (stamp.hour, WorkStr);             { Format time string }
    IF stamp.hour < 10 THEN
      StampStr := StampStr + '0' + WorkStr
    ELSE
      StampStr := StampStr + WorkStr;
    Str (stamp.min, WorkStr);
    IF stamp.min < 10 THEN
      StampStr := StampStr + ':0'
    ELSE
      StampStr := StampStr + ':';
    TimeStamp := StampStr + WorkStr + ap;
  END;


  FUNCTION Attribs : STRING;

     { Return file attributes as a string of indicators }

  VAR   attrib : STRING [6];

  BEGIN
    attrib := '......';
    WITH files DO BEGIN
      IF attr AND ReadOnly  <> 0 THEN attrib [6] := 'R';
      IF attr AND Hidden    <> 0 THEN attrib [5] := 'H';
      IF attr AND Sysfile   <> 0 THEN attrib [4] := 'S';
      IF attr AND VolumeID  <> 0 THEN attrib [3] := 'V';
      IF attr AND Directory <> 0 THEN attrib [2] := 'D';
      IF attr AND Archive   <> 0 THEN attrib [1] := 'A';
    END;
    Attribs := attrib;
  END;


  FUNCTION SizeInK : INTEGER;

     { Return allocated size of file in K }

  VAR  size : LONGINT;

  BEGIN
    IF files.size = 0 THEN
      SizeInK := 0
    ELSE BEGIN
      Size := files.size DIV BlockSize;
      IF size MOD BlockSize <> 0 THEN
        Inc (size);
      IF size = 0 THEN size := 1;
      SizeInK := (size * BlockSize) DIV 1024;
    END;
  END;


BEGIN  { Body of WriteFileInfo }
  Write   (files.name);     Gotoxy (21, whereY);
  Write   (TimeStamp);      Gotoxy (42, whereY);
  Write   (Attribs);        Gotoxy (53, whereY);
  Write   (files.size : 6); Gotoxy (64, whereY);
  KBytes := SizeInK;
  Writeln (KBytes : 3, 'K');

  TotalK := TotalK + KBytes;                { Accumulate totals }
  TotalBytes := TotalBytes + files.size;
  Inc (NFiles);
END;
{ ------------------------------------------------------------- }

PROCEDURE ListFiles (path : pathType; mask : maskType);

     { List files in currently selected directory using mask }


VAR  files     : SearchRec;
     heading   : STRING [160];
     lineCount : INTEGER;


  PROCEDURE StartPage;                       { Begin a new page }

  BEGIN
    ClrScr;
    Writeln (heading);
    Write   ('Name                Date        Time     Attrib');
    Writeln ('      Bytes     Size');
    LineCount := 3;
  END;


  PROCEDURE CountLines;   { Count lines, start new page if full }

  VAR   ch : CHAR;

  BEGIN
    Inc (LineCount);
    IF LineCount = 24 THEN BEGIN
      Gotoxy (1, 25);
      Write ('-- MORE --');
      ch := ReadKey;
      StartPage;
    END
  END;


  PROCEDURE ShowTotals;       { Show total bytes, K, files, etc. }

  VAR   free, size : REAL;

  BEGIN
    size := DiskSize(0);         { Size of default disk in bytes }
    free := DiskFree(0);
    Write   (NFiles, ' files, ', TotalBytes, ' bytes, ');
    Writeln (TotalK, 'K space');
    Write   (free:1:0, ' bytes free out of ', size:1:0, '
             total (');
    Write   ((((size-free) / size) * 100.0) : 5 : 2);
    Writeln ('% utilization)');
  END;


BEGIN  { Body of ListFiles }
  Heading := 'Directory for ' + mask + ' in ' +
              path + ':';
  StartPage;
  FindFirst (mask, AnyFile, files);
  IF DosError = 0 THEN
    REPEAT
      WriteFileInfo (files);
      CountLines;
      FindNext (files);
    UNTIL DosError <> 0;
  ShowTotals;
END;
{ --------------------------- }

PROCEDURE Separate (VAR path : pathType; VAR mask : maskType);

     { Break out path and mask from command-line argument }

VAR   p, c : INTEGER;

BEGIN
  path := ParamStr(1);
  mask := '';
  p    := Length (path);
  WHILE (path [p] <> '\') AND (p > 0) DO   { Find last \ in arg }
    Dec (p);
  IF p > 0 THEN BEGIN
    FOR c := (p + 1) TO Length (path) DO
      mask := mask + path [c];                 { copy file mask }
    IF p = 1 THEN
      path [0] := chr (1)                      { backslash only }
    ELSE
      path [0] := chr (p - 1);         { truncate path before \ }
  END;
END;
{ --------------------------- }

FUNCTION AllocInfo : INTEGER;

     { Returns the size of a disk allocation unit in bytes }

VAR   reg : registers;

BEGIN
  reg.ah := $1B;
  Intr ($21, reg);                        { DOS Int 21h, Fcn 1B }
  AllocInfo := reg.al * reg.cx;        { sec/cluster * sec size }
END;
{ --------------------------- }

PROCEDURE Check (VAR mask : maskType);

     { Check file mask, complete if necessary }

BEGIN
  IF mask [1] = '.' THEN                 { if form is '.EXT'... }
    mask := '*' + mask;
  IF pos ('.', mask) = 0 THEN        { if mask has no period... }
    mask := mask + '.*';
END;
{ --------------------------- }

BEGIN   { Main program }
  GetDir (0, oldDir);              { Save the current directory }
  BlockSize  := AllocInfo;                 { Initialize globals }
  TotalBytes := 0;
  TotalK     := 0;
  NFiles     := 0;
  Thru       := FALSE;
  fileMask   := '*.*';               { Initialize mask and path }
  searchPath := oldDir;

  IF ParamCount < 1 THEN              { No command-line arg, so }
    BEGIN                          { show all files in curr dir }
      ListFiles (searchPath, fileMask);
      Thru := TRUE;
    END;

  {$I-}                           { Disable auto error checking }
  IF not thru THEN BEGIN  { Is command SUB <dir> or SUB \<dir>? }
    searchPath := ParamStr(1);
    ChDir (searchPath);                  { Try to set directory }
    IF IOResult = 0 THEN BEGIN             { List if successful }
      {$I+}                          { Re-enable error checking }
      ListFiles (searchPath, fileMask);
      Thru := TRUE;
    END;
  END;

  {$I-}
  IF not thru THEN BEGIN            { Is command SUB <dir\*.*>? }
    Separate (searchPath, fileMask);
    ChDir (searchPath);                  { Try to set directory }
    IF IOResult = 0 THEN
      IF Length (FileMask) > 0 THEN BEGIN
        Check (fileMask);
        ListFiles (searchPath, fileMask);
        Thru := TRUE;
      END;
  END;

  IF not thru THEN BEGIN                  { Is command SUB *.*? }
    fileMask   := ParamStr(1);
    Check (fileMask);
    ListFiles (oldDir, fileMask);
    Thru := TRUE;
  END;

  IF not thru THEN
    Writeln ('PATH NOT FOUND');

  ChDir (oldDir);                    { Restore former directory }
END.









Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.