                        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.
