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 /'); 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.