[Back to TUTOR SWAG index] [Back to Main SWAG index] [Original]

                            Turbo Pascal for DOS Tutorial
                                 by Glenn Grotzinger
                         Part 9 -- Applications Development
                 All parts copyright (c) 1995-6 by Glenn Grotzinger

    Hello.  This time, I'm not going to go present the solution to part 8
    immediately, because I want to sue the part 8 problem to demonstrate
    applications development.  The basic reason I'm doing this is because
    programs should be designed and then programmed, normally, and not the
    other way around.  Normally in design, we have different tools available
    for us to use.  The ones I'm aware of are pseudocode and flowcharting.
    I can't cover flowcharting through text, but I can cover pseudocode.

    What is pseudocode?
    ===================
    Pseudocode is basically English-like descriptions of what is going on.
    I gave you an example of it in part 8 (with capital letters for emphasis,
    as I will continue to do so) as it pertains to a good line parsing
    function.  We will go through and design a solution to part 8 in this
    part.

    How to start out?
    =================
    It is good to start with the global way of things.  Let's start with a
    few facts we know from our knowledge from part 8.

            1) We're wanting listings of files.
            2) We're wanting stats on the operating system as well as the
               drive we access.

    Knowing our purposes in these two statements, we know we need usage of
    the DOS or WinDOS unit.

    Globally, by looking at the way we know the output needs to be, we can
    figure out that globally, the program is going to have to do the
    following:

            1) Write the ID of the program. {mydir by...}
            2) Parse the command-line to determine what needs to be done.
            3) Write resultant headers based on results in 2.
            4) If successful, while there are valid files, list the files
               as dictated in 2.
            5) Show the global drive and operating system stats.

    Starting to break down the global stuff and writing pseudocode
    ==============================================================
    We know this is the basic idea of things that we will have to do in
    order to complete this program.  We need to move from this down to
    the specific details.  Let's start with 1.

    We need to write the ID of the program (Global 1).
    --------------------------------------------------
    Also, we need to look at factors for initialization code.  We need
    some filespec variable clear...Also we know we need to count files
    and dirs, as well as set a pause flag to our default (false).

            PROCEDURE WRITEID;
               WRITE PROGRAM NAME AND AUTHOR NAME, COPYRIGHT INFO...
               SET PAGE COUNT TO # OF LINES USED (page pausing!)
            SET PAUSE TO FALSE, AND SET #DIRS AND #FILES TO 0.

    Parse the command-line (Global 2).
    ----------------------------------
    I will start globally, and move down to specifics here.  Keep in mind
    that the functions said that path/filename and parameter should be
    interchangeable (Function 5 as specified in the problem).  Here, what
    seems logical is to use the most definable command-line parameter and
    eliminate things down to the least specific.  Here, the command params
    ? and P are the most specific while the path is the least specific.

            IF PARAMETERS > 2 THEN SHOW SOME HELP AND QUIT.
            PULL ALL PARAMETERS INTO AN ARRAY (MAX 2).
            FOR EACH PARAMETER GIVEN TO PROGRAM IN ARRAY
             IF THE PARAMETER IS 2 CHARACTERS LONG AND THE
               FIRST CHARACTER IS - OR /
                 IF SECOND CHARACTER OF PARAMETER IS ? SHOW SOME HELP AND QUIT.
                 IF SECOND CHARACTER IS P, SET A PAUSE FLAG TO TRUE.
                   ELSE SHOW SOME HELP AND QUIT.
                 END-IF.
             ELSE SET FILESPEC TO THE COMMAND-LINE PARAMETER.
             END-IF.
            PARSE THE FILESPEC.

    Now, this is a general description of what's going on in part 2 of our
    global listing we made.  Now for specifics here.  The only ones I see
    are showing the help and quitting, and parsing the filespec.

    Show help and quit.
            WRITE THE HELP.
            SET PAGE COUNT TO NUMBER OF LINES USED (Page count for pause!)
            HALT THE PROGRAM.

    Parsing the filespec.  I gave you this pseudocode in part 8.

    Write resultant headers based on 2. (Global 3)
    ----------------------------------------------
            WRITE THE HEADER WITH PARSED PATH INTERPRETATION.

    List files as dictated by filespec in 2. (Global 4)
    ---------------------------------------------------
    We know, basically, in the DIR implementation we want to list all files
    except ones with the volume lables.  We know the file constants are
    additive but to ease things in typing, we can go ahead and add them up.
    All files except volumeID in the constants is equal to $37.  Also, we will
    keep in mind the following points that were brought up in the directions.

            1) (Function 1) Show us for each filename on one line a size,
               file attributes, date and time.
            2) (Function 3) All integers or longints > 999 should be
               delineated by commas, or periods, whichever you use.
            3) (Function 4) Write r for read-only, a for archive, s for
               system, and h for hidden.
            4) If we need to pause, be sure to implement it!
            5) Be sure to indicate if there are no files.

            START FILE LISTING.
            WHILE WE STILL HAVE FILES
               IF A FILENAME IS A DIRECTORY ($10) THEN
                  WRITE DIRECTORY NAME WIHT [DIR] DESIGNATION.
                  INCREMENT # OF DIRS BY 1.
               ELSE
                  WRITE FILENAME, FILESIZE, DATE, TIME AND ATTRIBUTES.
                  INCREMENT # OF FILES BY 1.
                  INCREMENT SIZE OF FILES IN DIR BY SIZE OF THIS FILE.
               END-IF.
               INCREMENT PAGELENGTH BY 1.
               IF PAGELENGTH > 23 AND PAUSE IS TRUE
                  WRITE PAUSE INDICATOR, READ FOR KEY, AND SET LINE
                  COUNTER BACK TO 0.
               END-IF.
            IF THERE IS A DOS ERROR OR THERE ARE NO DIRS AND NO FILES
               WRITE THAT THERE ARE NO FILES THERE.

    There's our global listing for part 4.  Now we need to consider the
    individual actions, basically, obtaining numbers with commas, the date,
    the time, and the file attributes.

    Numbers with commas (Code explained later)
            MAKE A STRING OUT OF THE NUMBER.
            FIND LENGTH OF NUMBER.
            WHILE THERE ARE MORE THAN 3 DIGITS TO CONSIDER
                 COUNT OFF 3 DIGITS FROM THE RIGHT TO THE LEFT.
                 PLACE A COMMA, AND SUBTRACT 3 DIGITS FROM LENGTH TOTAL.
            END-WHILE.
            COUNT OFF REST OF DIGITS.

    The date and time.  Basically, the issue here is pulling the information
    for the date, except the year, where we must consider the last 2 digits
    instead of 4 digits.  In getting the last 2 digits, we also need to keep
    in mind that the year 2000 will be coming in 4 years...  With the
    time, it will be in military time, so we may recycle code, say from part
    4 (It is always good to save code and copy so you do not have to
    invent the wheel and then turn around and invent it again...:)).  Note
    the repeated appearances of the strings "Make XXX a string." and "If
    XXX < 10, pad number with zero."  To pad a number, I mean, if I have
    9, then padding it with a zero would make it 09.  In good programming
    planning, if repeated code happens, isolate that code as a function or
    procedure to save lines of code.  AT the end, you will see that I have
    done that.

            UNPACK THE DATE AND TIME FROM THE SYSTEM.
            MAKE MONTH A STRING.
            IF MONTH < 10, PAD NUMBER WITH 0.
            MAKE DAY A STRING.
            IF DAY < 10, PAD NUMBER WITH 0.
            IF YEAR > OR = 2000
               2YEAR IS YEAR - 2000
            ELSE
               2YEAR IS YEAR - 1900
            MAKE 2YEAR A STRING.
            IF YEAR < 10, PAD NUMBER WITH 0.
            FINAL-FILE-DATE IS MONTH-DAY-2YEAR.
                              (or DAY-MONTH-2YEAR, whichever you prefer)

            IF HOUR > OR = 12
               HOUR IS HOUR - 12
               MERIDIAN IS PM.
            ELSE
               MERIDIAN IS AM.
            END-IF.
            IF HOUR = 0 THEN HOUR = 12.
            MAKE HOUR A STRING.
            IF HOUR < 10, PAD STRING WITH 0.
            MAKE MIN A STRING.
            IF MIN < 10, PAD STRING WITH 0.
            TIME IS HOUR:MINmeridian.

    Get file attributes.  I gave hint #6 for this part to build your string
    and said I'd explain later.  A string in pascal (a pascal string),
    actually is stored using length + 1 bytes.  The first byte is a number
    representing the total length of the string.  The rest of it is the
    string.  Since we use a background of -'s, that's all we need to start
    the string from...Then use proper position to assign things...and again
    using the file attribute constants, remembering that they are additive.
    Starting from the largest to smallest...Say, we want to reassign the
    3rd character of STR, we just say str[3] := 's' ro something like that.

            SET STRING TO ----.
            IF FILEATTR >= $20 THEN
               FILE HAS ARCHIVE BIT.
               FILEATTR = FILEATTR - $20.
            END-IF.
            IF FILEATTR >= $04 THEN
               FILE HAS SYSTEM BIT.
               FILEATTR = FILEATTR - $04.
            END-IF.
            IF FILEATTR >= $02 THEN
               FILE HAS HIDDEN BIT.
               FILEATTR = FILEATTR - $02.
            END-IF.
            IF FILEATTR >= $01 THEN
               FILE HAS READ-ONLY BIT.
               FILEATTR = FILEATTR - $01.
            END-IF.

    Show the global stats and operating system info (Global 5)
    ----------------------------------------------------------
    Basically, this is a write operation.  But we need to know how to get
    the information...

    Volume label.  Using the hint in the problem.

            SEARCH FOR FILE WITH VOLUMEID ATTRIBUTE IN ROOT DIR OF
               DRIVE WE ARE ACCESSING.
            IF THE FILE EXISTS, FILENAME IS THE VOLUMEID
            ELSE LEAVE IT BLANK.

    Total files and total dirs and total size of the files listed are
    simply writing variables (be sure to run these numbers through the
    comma delineator function).

    Total size used on drive.  The drive is designated as a number, and the
    nice thing about our pase_filespec thing is that the first character
    always ends up being the drive letter of the drive we want to work with.
    So, to go from drive letter to number, we can always set a constant
    guide string (sort of like my suggestion in part 7 to do this for bases
    > 11 as to ease things) defined as: ABCDEFGHIJKLMNOPQRSTUVWXYZ.  I know
    this is probably overkill, but it will cover things OK so we don't have
    to write a large case statement.  Run this one through the delineator.

            DRIVE NUMBER IS LETTER POSITION IN CONSTANT STRING.
            FREE-ON-DISK FOR DRIVE NUMBER.

    Total size on drive.  Similar to total siz eused.  Uses constant guide
    string...also ran through delineator.

            DRIVE NUMBER IS LETTER POSITION IN CONSTANT STRING.
            TOTAL-ON-DISK FOR DRIVE NUMBER.

    DOS version.  Getting this is exactly like described in part 8.  Nothing
    out of the ordinary.

    This finishes up my pseudocode description for the part 8 problem.  Now,
    for my best solution that I could come up, with the suggestion (probably
    could be faster if I didn't do it, but I went ahead and used the save
    format for the record as some help, and allowed for 999,999,999 as a
    possible total file size in the layout -- too big to really worry, though
    it probably slows it up a little...)

    Keep in mind too, that this pseudocode was revised, after I found out that
    some of my original statements turned out to be wrong in my logic planning.
    It's OK to create incorrect pseudocode.  It is only a planning tool, and
    does not have to be correct.  The only thing anyone will really care about
    being correct is your final source code -- correct meaning that it works
    properly.

    The source code for my implementation of MYDIR.

    program part8; uses dos;

      { a dir command.  Supports command params /? and /P.
        shows filename, filesizes with commas, time, date, attributes.
        For total, shows drivesize in bytes, bytes used on drive.
        Volume label of drive, total number of files, total numbers of
        directories. }

      type
        writerec = record     { format to write out the dirinfo }
          filename: string[12];
          filesize: string[11]; { largest size => 999,999,999 bytes }
          filedate: string[8];
          filetime: string[7];
          fileattr: string[4];
        end;

      var
        dirinfo: searchrec;
        writeinfo: writerec;
        params: array[1..2] of string;
        i: integer;
        pause: boolean;
        filespec: string;
        dirs, files, totalsize: longint;
        pagelen: integer;

      function parse_filespec(filename: string):string;

        const
          all_files = $37;
          { all file constants in base 16 added together but VolumeID }
        var
          dir: dirstr;   { required types for some of the commands as }
          name: namestr; { defined in the DOS unit. }
          ext: extstr;
          attr: word;    { attribute must be a word. }
          f: text;       { required for a command we had to assign file for }

        begin
          filename := fexpand(filename);  { expand filename }
          if filename[length(filename)] <> '\' then { if end not \ }
            begin
              assign(f, filename);
              getfattr(f, attr);            { get the file attribute }
              if (doserror = 0) and (attr = $10) then
                filename := filename + '\'; { if it's a directory put \ }
            end;
          fsplit(filename, dir, name, ext); { split filename up. }
          if name = '' then name := '*'; { if it's still a directory, }
          if ext = '' then ext := '.*';  { specify ALL FILES }
          parse_filespec := dir + name + ext;  { re-form filename }
        end;

      function zero(innum: integer):string;
        var
          tstr: string[2];
        begin
          str(innum, tstr);
          if innum < 10 then
            tstr := '0' + tstr;
          zero := tstr;
        end;

      procedure showid;
        begin
          writeln('MYDIR (c) 1996 by Glenn Grotzinger.');
          writeln;
          pagelen := pagelen + 2;
        end;

      procedure writeheader(filespec: string);
        begin
          writeln('File listing for: ', filespec);
          writeln;
          pagelen := pagelen + 2;
        end;

      function number(n: longint):string;
        var
          i, j: integer;
          s, r: string;
          m: integer;
        begin
          str(n, s); { get the longint to a workable string }
          r := '';   { r is a holding string for our delineated number }
          i := length(s);
          { set an integer to the length of our number.  We will be going
          from the right end and moving to the left in placing our delin-
          eations, and building r from the right to the left. }
          if i > 3 then begin
          while i > 3 do   { while we don't have 3 numbers left }
            begin
              for j := i downto i-2 do  { count off 3 digits and move to r}
                r := s[j] + r;
              r := ',' + r;  { write a comma or period to r }
              i := i - 3;    { subtract 3 digits }
            end;
          for j := i downto 1 do
          { we only have 3 digits left, or less now.  Just count off the
            digits }
            r := s[j] + r;
          number := r;  { feed r to the function }
          end
        else
          number := s;
        end;

      procedure getdatetime(dirinfo: searchrec; var writeinfo: writerec);
        { type uses datetime record }
        var
          dt: datetime;
          a,b,c: string; { temp strings for use }
          tmpyr: integer; { we need to define this one to do the year }
        begin
          unpacktime(dirinfo.time, dt); { unpack date and time }
          { start with date }
          { month }
          a := zero(dt.month);
          { day -- same as month }
          b := zero(dt.day);
          { year -- reported as 4 digits.  We need to get it down to 2. }
          { keep in mind the valid range that TP will report year. }
          if dt.year > 1999 then      { if year 2000 or above }
            tmpyr := dt.year - 2000
          else                        { else would be before 2000 }
            tmpyr := dt.year - 1900;
          { now that we have our last 2 digit year, perform similar to day
            & month }
          c := zero(tmpyr);
          writeinfo.filedate := a + '-' + b + '-' + c; { set final date }
          { now start with the time -- it's reported as military time --
            we need to deal with it as such }
          { military time determination }
          if dt.hour >= 12 then
            begin
              dt.hour := dt.hour - 12;
              c := 'pm';
            end
          else
            c := 'am';
          if dt.hour = 0 then
            dt.hour := 12;
          { hours -- deal with as we did days now }
          a := zero(dt.hour);
          { minutes }
          b := zero(dt.min);
          writeinfo.filetime := a + ':' + b + c;
        end;

      function getfileattr(dirinfo: searchrec):string;
        var
          left: word;
          str: string;

        begin
          str := '----';
          left := dirinfo.attr;
          if left >= $20 then  { archive file }
            begin
              str[2] := 'a';
              left := left - $20;
            end;
          if left >= $04 then  { system file }
            begin
              str[3] := 's';
              left := left - $04;
            end;
          if left >= $02 then  { hidden file }
            begin
              str[4] := 'h';
              left := left - $02;
            end;
          if left >= $01 then  { read-only file }
            begin
              str[1] := 'r';
              left := left - $01;
            end;
          getfileattr := str;
        end;

      procedure writefinfo(dirinfo: searchrec;writeinfo: writerec);
        var
          i: integer;
        begin
          with writeinfo do
            begin
              filename := dirinfo.name;
              filesize := number(dirinfo.size);
              getdatetime(dirinfo, writeinfo);
              fileattr := getfileattr(dirinfo);
              write(filename);
              for i := length(filename) to 12 do
                write(' ');
              writeln(filesize:12, filedate:12, filetime:12, fileattr:12);
            end;
        end;

      function getvolumeID(drive: char):string;
        { volume label exists as a file in the root directory with a special
          attribute called VolumeID and the name of the file is the volume
          label }
        var
          fstr: string;
          dinfo: searchrec;
        begin
          fstr := drive + ':\*.*';
          findfirst(fstr, VolumeID, dinfo);
          if doserror = 0 then  { Volume label exists so...}
             getvolumeID := dinfo.name
          else
             getvolumeID := ''; { leave it blank if no volume label }
        end;

      procedure showhelp;
        begin
          writeln('Help:');
          writeln('  MYDIR <filespec> /<parameters>');
          writeln('  filespec is the filename/dirname(s) we want to list.');
          writeln('  parameters are ? or P (case insensitive)');
          writeln('       ? --> This help.');
          writeln('       P --> pause on screen page.');
          halt(1);
       end;

      function bytesfree(drive: char):longint;
        const
          guide = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        var
          dno: integer;
        begin
          dno := pos(upcase(drive), guide);
          bytesfree := diskfree(dno);
        end;

      function bytesthere(drive: char):longint;
        { in TP, this won't work if you have a partition > 1 gigabyte }
        const
          guide = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        var
          dno: integer;
        begin
          dno := pos(upcase(drive), guide);
          bytesthere := disksize(dno);
        end;

      begin
        { initialization code }
        showid; { should be the first thing we do }
        pause := false;
        filespec := '';
        totalsize := 0;
        dirs := 0;
        files := 0;
        { check # of parameters }
        if paramcount > 2 then
          showhelp;
        { pull in parameters }
        for i := 1 to paramcount do
          params[i] := paramstr(i);
        { check 'em for ? and P parameters, and filespec }
        for i := 1 to paramcount do
          if (length(params[i]) = 2) and
             (params[i][1] in ['-','/']) then
             case upcase(params[i][2]) of
               '?': showhelp;
               'P': pause := true;
             else
               showhelp;
             end
          else
            filespec := params[i];
        filespec := parse_filespec(filespec);

        { go into start }
        writeheader(filespec);
        findfirst(filespec, $37, dirinfo);
        while doserror = 0 do
          begin
            if dirinfo.attr = $10 then
              begin
                write(dirinfo.name);
                for i := length(dirinfo.name) to 12 do
                  write(' ');
                writeln('[DIR]':49);
                inc(dirs);
              end
            else
              begin
                writefinfo(dirinfo, writeinfo);
                inc(files);
                totalsize := totalsize + dirinfo.size;
              end;
            inc(pagelen);
            if (pagelen > 23) and (pause) then
              begin
                write('--Pause--');
                readln;
                pagelen := 0;
              end;
            findnext(dirinfo);
          end;
        if (doserror in [1..17]) or ((dirs = 0) and (files = 0)) then
          writeln('No files found.');
        writeln;
        writeln('Volume label: ', getvolumeid(filespec[1]), 'Total Files: ':20,
                 number(files), 'Total Dirs: ':20, number(dirs));
        writeln('DOS Version: ', lo(dosversion), '.', hi(dosversion));
        writeln;
        writeln(number(totalsize), ' bytes.');
        writeln(number(bytesfree(filespec[1])), ' bytes free out of ',
                number(bytesthere(filespec[1])), ' total bytes.');

      end.
    Next Time
    =========
    We will cover reading and writing from binary files in part 10, as well
    as use of units, overlays, and include files in programming.  Part 10
    will be a long program, which will stress the ideas represented here
    mainly, but will also involve material in part 10.  It will be a tedious
    one, but does not require a lot of programming knowledge to complete.
    This problem will also be set up for a programming contest.  If there
    are any comments, please write ggrotz@2sprint.net.

[Back to TUTOR SWAG index] [Back to Main SWAG index] [Original]