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 ,,} 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 -> 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();. 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 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.