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

                            Turbo Pascal for DOS Tutorial
                                  by Glenn Grotzinger
                     Part 8 -- DOS file functions (special topic 1)
                   All parts copyright (c) 1995 by Glenn Grotzinger

    Here's a solution to part 7....

    program part7;

      { demos 2 functions defined in part 7 to be written.  Convert base 10
        to anybase, and convert anybase to base 10 }

      function power(int, ord: integer):longint;
        { support function required for xbase2dec.  Simplistic
          implementation of taking a power. }
        var
          i, endit: longint;
        begin
          endit := 1;
          for i := 1 to ord do
            endit := endit * int;
          power := endit;
        end;

      function dec2xbase(int: longint; base: integer):string;
        { converts base 10 to any base < 37 }
        const
          numguide: string = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
          { the guide string I mentioned as a hint }
        var
          i: integer;
          hold, chkprod1, chkprod2: longint;
          endstr: string;
        begin
          if base > 36 then
            dec2xbase := '!' { a signal character to say our function failed }
          else
            begin
              endstr := '';
              hold := int;
              while chkprod1 <> 0 do
                begin
                  chkprod1 := hold div base; { using method described when I }
                  chkprod2 := hold mod base; { demoed doing it manually }
                  endstr := numguide[chkprod2 + 1] + endstr;
                            { actual representation }
                  hold := chkprod1;
                end;
              dec2xbase := endstr;
            end;
        end;

      function xbase2dec(int: string; base: integer):longint;
        { converts any base < 37 to base 10 }
        const
          numguide: string = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        var
          i, powr: integer;
          endresult: longint;
          intlength: integer;
        begin
          if base > 36 then
            xbase2dec := -1 {signal fucntion has failed }
          else
            begin
              endresult := 0;
              i := 1;
              powr := length(int) - 1;
              while i <> length(int)+1 do   { compute back to base 10 }
                begin
                  endresult := endresult + (pos(int[i], numguide)-1) *
                               power(base, powr);
                  i := i + 1;
                  powr := powr - 1;
                end;
            end;
          xbase2dec := endresult;
        end;

      var
        numread, numbase: longint;
        convbase: string;
      begin
        write('Enter a number (base 10): ');
        readln(numread);
        write('What base do you want to convert to? ');
        readln(numbase);
        writeln;
        convbase := dec2xbase(numread, numbase);
        if convbase = '!' then
          writeln('Use a base less than 37')
        else begin
          writeln(numread, ' (base 10) is ', convbase, ' (base ', numbase, ').');
          numread := 0;
      { required setting of initial read var to 0 to prove our functions work }
          writeln('To check: ', convbase, ' (base ', numbase, ') is ',
                   xbase2dec(convbase, numbase), ' (base 10).');
        end;
      end.

    Now on with the show....This is going to be more of a special topics practice
    thing.  As a note, all of the commands I use here will require you to use
    the dos or windos unit....also, have your TP Programmers Reference handy to
    look up several of the basic functions that I will list here.

    A simple if successful...
    =========================
    How do we check if any of these, or our prior reset/rewrite calls are
    successful?  We toggle the I compiler option before a valid file oper-
    ator, then we check the variable in IOResult.  If IOResult <> 0, then
    we know there is a problem.  For example, how do we check if a file
    exists on the drive before we read from it?

    function exists(filename: string):boolean;
      var
        ourfile: text; { can be defined to be any type we want }
      begin
        assign(ourfile, filename);  { let the program know about the file }
        {$I-}reset(ourfile);{$I+}   { toggle I, on a reset. Both MUST be there }
        if IOResult <> 0
        { did something go wrong?  With reset, if file did not exist, something
          would be wrong }
          exists := false           { indicate file doesn't exist }
        else  { file exists }
          begin
            close(ourfile); { we need to close the file since if it's there, }
            exist := true;  { we just opened it.  Also, indicate it's there. }
          end;
      end;

    This method can be used with any file function like that as long as the
    system is aware of the file, to indicate success, possibilities, etc.,
    in whatever logical means the command indicates.  For reset, one logically
    can say that if something goes wrong with it, the file isn't there, or the
    path is invalid.  For rewrite, one logically can say if the path isn't
    right or available, or if there's no disk space, or a variety of reasons.

    The I compiler directive indicates turning ON or OFF input/output checking.
    The default for most compilers is {$I+}.  As compiler directives go, we
    must toggle it back, because the area after the change is in effect, and
    we only want it for the case of the command we want to check.  If I/O
    checking is on, the program would end in a run-time error (you may have
    noticed this, if you have tried to access non-existent files in your
    experimentation with reading/writing text files). If I/O checking is off
    ({$I-}, the result of the command is logged in a global variable named
    IOResult.

    Being able to subvert run-time errors is good to be able to do, especially
    for a DOS file function type program, where you even deal with files.
    If the user specifies an incorrect file to read, you can return an intel-
    ligible, user-understandable error message that the filename they gave
    does not exist on the drive.

    A list of run-time error possibilities may be found on page 260 of the
    TP7 programmers reference.

    Straight Forward Things
    =======================
            I will list several commands for working with files and DOS
    which are relatively straight-forward to use.  Unit used, then command,
    then a short description.  Detailed descriptions follow if needed.  I
    will mainly cover the DOS unit variants.  If you use TPW, look up the
    WinDOS variant equivalents...Be sure to look each of these up in any
    case.... By all means, play with each of these to understand them.
    Notes on some of these things will appear later.

    System: ChDir(Str: string);  { changes the current directory to path in Str }
    DOS/WinDos: DiskFree(Drive: byte):longint; { free bytes on Drive }
    DOS/WinDos: DiskSize(Drive: byte):longint; { total bytes on Drive }
    DOS/WinDos: DosVersion: word; { tells us what version of DOS we have }
    DOS: EnvCount: integer; { how many environment strings? }
    DOS: EnvStr(index: integer): string; {return a specific environment string}
    System: erase(file: filetype);  {erases an external file}
    System: FilePos(file: filetype): longint; {returns current position in file}
    System: FileSize(file: filetype): longint; {returns a file's size}
    DOS: FSearch(filename: PathStr; dirlist: string):Pathstr {search for a file}
    DOS: FSplit(...  {split a filename into a dir, name, and ext}
    DOS/WinDos: Getdate(... { gets the current date of the operating system.}
    System:GetDir(drive; str: string); {gets current directory}
    DOS: GetEnv(...  {gets the specified environment variable}
    DOS/WinDos: GetFAttr (... { returns file attributes of a file }
    DOS/WinDos: GetFTime (... { get the date and time of a file }
    DOS/WinDos: GetTime(... { get current time }
    System: Halt(code); { quits program immediately with an errorlevel }
    System: MkDir(str: string); { makes a directory named str }
    DOS/WinDos: PackTime(... { packs a time/date }
    System: Rename(file: filetype); { renames an external file }
    System: RmDir(str: string); { removes a dir named str }
    System: Seek(file: filetype); { finds an element # in a file }
    DOS/WinDos: SetDate( ... { sets the date on the machine }
    DOS/WinDos: SetFTime(... { sets the date and time of a file }
    DOS/WinDos: SetTime(...  { set the time on the machine }
    DOS/WinDos: UnpackTime(... { unpacks a time/date to datetime record }

    Notes on some of the commands listed above that aren't that self-
    explanatory
    ---------------------------------------------------------------------
    Diskfree(Drive: byte): longint;
    DiskSize(Drive: byte): longint;
         Drive is a numerical byte: 0 is the current drive.
                                    1 is A drive.
                                    2 is B drive.
                                    3 is C drive.
                                    and so on and so forth.

    NOTE: DiskFree and DiskSize are limited from what I understand to < 1 gig?
    (Please correct me if I am not quoting this right).  I know there is a
    limit there somewhere...

    DosVersion(version: word);
         You have to use HI and LO functions here as described in part 7.
         The high order of this expression would be a minor revision number
         and the low order of this expression would be a major revision
         number.  For example, if you have DOS 6.20, the high order would be
         20, and the low order would be 6.

    Any expression that uses packed and unpacked date and time.
         There is two of the functions listed above called packtime and
         unpacktime.  There is a special record already defined in DOS
         called DateTime (or TDateTime in WinDOS).  Both of these are defined
         like this:

         DateTime { or TDateTime -- they're the same } = record
            Year, Month, Day, Hour, Min, Sec: word;
         end;
         A packed time is stored as a longint;

    Good use of an erase/rename, etc, etc...
         Make a procedure that does the assign, and error checks, then do
         the command, if the command requires the file to be an assigned
         file variable.  For example:

         procedure deletefile(filename: string);
           var
             afile: text; { It can be anything we will find out }
           begin
             assign(afile, filename);
             {$I-}erase(afile);{$I+}
             if IOResult <> 0 then
               writeln('Erasure unsuccessful.');
             else
               writeln(filename, ' has been erased from your drive!');
           end;

    Note: This erase command is recoverable by use of an undelete program.
    The rewrite is not (all you'll see is a 0 byte file that replaces the
    file you rewritten).

    Parameter Passing
    =================
    You may have noted that we can get parameters in from the command-
    line to some programs we have used, such as PKZIP and PKUNZIP.
    We can write and design our programs to do such a similar thing.

    Paramcount: integer;  { holds the number of command-line parameters used }
    Paramstr(num: integer): string { specific command-line parameter }

    An example: Write a text file out to the screen using full error-checking,
    and taking the filename from the command-line.  Describes a command-line
    parameter describing a single file.

    program typefile;
      var
        param1, instr: string;
        thefile: text;
      begin
        if paramcount <> 1 then  { if there is not one command-line parameter }
          begin
            { show some help to the user }
            halt(1);    { quit the program right here }
          end;
        param1 := paramstr(1); { corresponds to %1 in batch file processing }
        { always good to do -- found that addressing the function directly as
          a string causes problems }
        assign(thefile, param1);
        {$I-}reset(thefile);{$I+}
        if IOResult <> 0 then
          begin
            writeln('This file doesn''t exist!');
            halt(1);
          end;
        readln(thefile, instr);
        { if the file is there, no need to error check reads, if our logic
          is correct }
        while not eof(thefile) do
          begin
            writeln(instr);
            readln(thefile, instr);
          end;
        writeln(instr);
      end.

    Now, what if we want to determine a file based on a command-line parameter.
    This isn't exactly complete error checking, as we aren't processing whether
    the file we specify is a directory or not (EVERYTHING to DOS is a file of
    some sort, including a directory, we can't exactly TYPE a directory...).
    That was a demo of picking up command-line parameters, here's how we
    process a filename parameter so we are addressing the correct thing,
    EVEN a directory, and series of files (Remember wildcards in DOS?  The
    usage of * and ? in specification of files).

    We use Fexpand, Fsplit, and GetFattr with doing this, as well as some DOS
    defined constants which distinguish between different attributes of files.
    They are additive, BTW.

         File Attribute Constants
         ------------------------
            ReadOnly       $01
            Hidden         $02
            System File    $04
            Volume ID      $08
            Directory      $10
            Archive        $20
            AnyFile        $3F

    Look at the DOS dir command for an example.  Vary what you type in,
    including directory names, and variants (.., \, and just a dirname).
    See how it acts.  That's what we want to emulate.  I will lead in
    to part 9 by placing something used in planning called pseudocode
    here to do such a thing.

    EXPAND FILENAME ON COMMAND LINE TO FULL DIR, NAME, AND EXTENSION.
    IF THE END OF THE FILENAME DOES NOT HAVE A \ THEN
        GET FILE ATTRIBUTE.
        IF NO DOSERROR AND THE FILE IS A DIRECTORY
           ADD A \ TO THE END OF THE FILENAME.
    END-IF.
    SPLIT FILENAME INTO PROPER DIR, NAME AND EXT.
    IF NO NAME, THEN PLACE A * THERE.
    IF NO EXTENSION, THEN PLACE A .* THERE.
    FULL FILENAME = DIR, NAME, AND EXTENSION TOGETHER.

    You will have to interpret this into viable code for the programming
    problem at the end of the part.

    Listing a Sequence of Files
    ===========================
    Now, to answer the question, what if we want to actually do something
    with multiple files (specified with * and ?), instead of just one file
    (if we want just one file, if we do the i/o checking on reset or
    rewrite, we will get an error if they use * or ? in the name.).

    We need to define the usage of a record as a searchrec type for DOS
    (or TSearchRec for WinDos).

          searchrec = record
             fill: array[1..21] of byte;
             attr: byte;       { additive from the file attribute constants }
             time: longint;    { Packed Time }
             size: longint;    { Size of file }
             name: string[12]; { name of file }
          end;

    This record, as well as DateTime is defined in the DOS unit, and we don't
    NEED to define these in our programs...They are used with the FindFirst
    and FindNext procedures, which are demoed below, listing all filenames
    in the current directory.

    program tutorial19; uses dos;
      var
        fileinfo: searchrec;
      begin
        findfirst('*.*', $37, fileinfo);
        while doserror = 0 do         { 18 = no more files }
          begin
            writeln(fileinfo.name);
            findnext(fileinfo);
          end;
      end.

    As a note: . and .. are filenames.  . is the base of the file system, ..
    is the next directory up....

    Executing a Program
    ===================
    You can execute a program from your program by using the exec procedure.
    You also have to use the $M compiler directive.  It's a directive to
    determine the stack size, as well as the minimum and maximum heap size.
    You must set this for to get the memory to run the program.  The format
    of the $M compiler directive is this:

      {$M <stacksize>,<minheapsize>,<maxheapsize>}

    If you don't set this, maxheapsize defaults to all of your conventional
    memory, definitely not good if we want to give the memory to a program
    to even run (nothing will happen if the program doesn't have the memory
    to run).  For example, we will use {$M $4000,0,0}.  Here is the format
    of the EXEC command.

      exec(<command interpreter path>, <command>);

    command interpreter path -> Where is COMMAND.COM?  We can use getenv
    to get the environment variable named COMSPEC to get this.  I've had
    people try to rebunk me to say that you could just say exec(<command>);.
    It DOES NOT WORK! (the compiler says something about expecting a ,.
    You must do it this way!) -- I'm only trying to head off this issue.

    command -> This must be /C + whatever command you call....

    Another command used in combination with this is called swapvectors.
    It is a parameterless procedure which makes sure our program we call
    doesn't literally stomp all over anything we may have done with the
    system.  We call it before and after our exec procedure.

    Here's an example:

    {$M $4000,0,0}
    program tutorial20; uses dos;
      var
        command: string;
      begin
        write('Enter a DOS command: ');
        readln(command);
        command := '/C' + command;
        { we must put the /C there to satisfy COMMAND.COM }
        swapvectors;
        exec(getenv('COMSPEC'), command);
        swapvectors;
        if doserror <> 0 then
          writeln('Dos Error ', doserror, ' occurred.')
        else
          writeln('Successful shell to DOS.');
      end.

    Use DosExitCode in addition to doserror to determine the results of the
    exec'd procedure.  This function returns a word, which we have to use
    the HI and LO functions on.  LO code results correspond to errorlevel
    returned out of DOS (remember batch file programming?).  A list of HI
    code results follow:

                0       Normal Termination
                1       Terminated by Ctrl+C
                2       Terminated by Device Error
                3       Terminated as a TSR.

    Conclusion
    ==========
    We have covered the major issues in DOS file commands, as well as
    listing some of the straight-forward commands that are in Pascal
    to work with DOS.  If you looked those up, you would have found that
    most of those things are pretty straight-forward.  Here is a practice
    programming problem that duplicates the function of the DIR command
    in DOS, showing us files with specified information.  It will be a
    clone with different functions, though.  Let us see....

    Practice Programming Problem #8
    ===============================
            Write a program in Pascal and entirely Pascal that will show
    us a listing of all files in a directory given on the command-line.
    It should also support command line parameters that change function,
    such as '/?' and '/P'.  Those are the two command-line parameters that
    we should support (be sure we make it case insensitive).  /? or -?
    will show us help and a listing of who wrote the program and then
    terminate the program.  /P or -p should make it so we pause the screen
    output on each page.  It also should handle any error-checking that
    it may need to perform as presented in this part.

    Functions:
    1) Show us for each filename on one line, a size, file attributes, date
    and time.
    2) For the whole listing of files, show us: The volume label, Size of the
    drive we listed, bytes free on the drive we listed, total number of
    files listed, total number of directories listed, and DOS version that
    is being used at the current time.
    3) All integers or longints > 999 should be delinated by commas, or
    periods, whatever you use.
    4) Write r for read-only, a for archive, s for system, h for hidden.
    Put the proper ones that apply by each file that it lists.
    5) For reading command-line parameters, be sure to make the order
    non-specific.  For example, MYDIR c:\windows /p and MYDIR /p c:\windows
    should accomplish the same thing, viewing the windows directory with
    page pausing.

    Hints:
    1) If a file with a volumeID attribute exists in the main directory of
    a drive, the name of the file is the volumeID of the drive.
    2) Work with the number as a string and go from the right end to the
    left end to place commas as strings for function point 3.
    3) You may want to use a designed record type to hold the final data
    you are going to write as you go obtain it to make things easier.
    4) This DIR listing command you are writing should function from the
    command-line with regards to filespec exactly like the DOS dir
    command.  Check this one by playing around with the DOS dir command.
    5) Hint for the #5 function.  Hold the parameter strings in an array
    and write some code to differentiate between command-line params and
    the actual path you want to view.
    6) For the string with the attributes, build your string.  You can
    directly address and build a string with certain portions as long
    as you start with a valid length of something.  I will explain this
    in the next part.

    Sample output for yourdir command.
    ----------------------------------
    C:\>mydir /?

    MYDIR (c) 1996 by Glenn Grotzinger.  { put your name here, of course }

    Help:
      MYDIR <filespec> /<parameters>
      filespec is the filename/dirname(s) we want to list.
      parameters are ? or P
           ? --> this help.
           P --> pause on each screen page.

    C:\>mydir

    MYDIR (c) 1996 by Glenn Grotzinger.

    File listing for: C:\*.*

    .                                                  [DIR]
    ..                                                 [DIR]
    CONFIG.SYS              122    12-12-95    03:45pm  rash
    DESCRIPT.ION            182    12-28-95    12:14am  -a--
    ARCHIVE.ZIP      14,432,322    03-24-93    09:15pm  r--h

    Volume label:          Total files: 3      Total dirs: 2
    DOS Version: 6.22

    14,432,626 bytes.
    214,232,123 bytes used out of 543,212,123 total bytes.

    You get the general idea....BTW, for me, this one is 310 lines..
    The next practice program given will be in part 10.  This one
    and the one in part 10 will be the longest and probably most
    complex ones in the whole set of pascal tutorials.  It would be
    good for ANYBODY who wants the practice to do these.

    Next time
    =========
    We will do the first part of the tutorial covering applications
    development.  In doing that, we will develop this DIR equivalent
    that I posed above.  Do practice, though, and do this one.  Any
    comments, questions, etc, may go to ggrotz@2sprint.net.

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