Q L H A C K E R ' S J O U R N A L =========================================== Supporting All QL Programmers =========================================== #28 July 1998 The QL Hacker's Journal (QHJ) is published by Tim Swenson as a service to the QL Community. The QHJ is freely distributable. Past issues are available on disk, via e-mail, or via the Anon-FTP server, garbo.uwasa.fi. The QHJ is always on the look out for article submissions. QL Hacker's Journal c/o Tim Swenson 2455 Medallion Dr. Union City, CA 94587 swensontc@geocities.com http://www.geocities.com/SilconValley/Pines/5865/ ****** Note New Mailing Address ******** EDITORS' FORUMN I have not received much feedback on the Qliberator Source Book idea I mentioned in the last issue, and I have received basically no "helpfull hints". So, my plan is to write what I can, put it out on the Net, and see what feedback comes from it. If I make any mistakes, I'm sure there are many willing to point out my failings :-). Sometimes the best way to get an answer to a question is to give a wrong answer, then many will pop up to give the real answer. I'll probably put in questions that I have in the hope that they too will get answered. A part of the material going into the Source Book is coming from stuff I'm writing for this newsletter. I have taken the article on adding Config Blocks to Qlib program and expanded it quite a bit. A couple of articles from this issue will also go in. I also plan to have sections that touch on the various SuperBasic extentions and programming aids available to the SuperBasic programmers. In other news, besides having past issues of the QHJ available on my web page, I've added a number of articles that I've written for other newsletters. Check the link from the main page. STRUCTURED SUPERBASIC 2.6 Structured SuperBasic is a utility that has been printed a couple of times in this newsletter. I have recently dusted the program off and completed a new version. SSB 2.6 has all of the functionality of SSB 2.5, but I have added a lot of error checking, fixed a few bugs, added Config Block support, Command Line support, Environment Variable support, and compiled it with Qliberator. The manual has been expanded from 5 pages to 16 pages. The whole package has been zipped and should be available your local QL BBS and the Internet. It can be downloaded off my web page at http://www.geocities.com/SiliconValley/Pines/5865/. For those that don't remember what SSB is, it is a filter program that takes SuperBasic code, written in the form for SSB and converts it to full SuperBasic code. SSB allows for no line numbers, blank lines between code segments, conditional compilation, Include files, and other features. SSB allows for more readable and maintainable code. Download the package, unzip it, and give it a try. REVISION CONTROL SYSTEM (RCS) Whether it's source code or system configuration files, it's nice to be able to keep track of changes made to files over time. The Revision Control System is a collection of programs that keep track of different versions (revisions) of text files. A special file is created by RCS that contains information about the changes in a file and allow the user to get back to any revision of the file. RCS was designed primarily for programmers to keep track of what changes were made to various source files. It was derived from an earlier Source Code Control System (SCCS) and was first developed for the UNIX operating system. Since RCS came from a UNIX background, it understands the concepts of different users accessing the same files and allows "checking out" files and locking them from being changed by other users. RCS can be used by a number of programmers all working on the same code. It keeps track of who has what file "checked out" and who makes what changes. Although most QL programmers program by themselves, RCS can help the QL programmer bring a form of discipline to their programming. This is especially usefull for what can be called "full production" programs; commercial or freeware. Given the long life of QL programs (I'm still using programs written more than 10 years ago), RCS can keep track of what changes were made from revision to revision and why. A programmer can keep make a rule that each RCS revision be a bug fix and the reasons for the fix can be logged as part of the revision history. Enough blathering on, now to jump right into what RCS is and how it works. Original File - This is the text or source code file that you want to keep track of. It can be any text file. Revision File - This is a single file that contains the original text/source file and all of the revision history. It has the same name as the original file except that a ',v' is appended to the end. If the original file is 'test_c', then the revision file is called 'test_c,v'. A revision file keeps track of only one original test/source file. If you have 3 source code files that make up a single executable, then RCS will have three revision fills. Check-In - All revisions to a file are put into the revision file by checking them in. The program 'ci' is used to do this. 'ci' is used to either create a new revision file or to check-in a new revision into an existing revision file. Once a file has been checked into the revision file, it is deleted. Check-Out - When you want to edit a text/source file that has been checked in to the revision file, you use 'co' (check-out) to extract the text/source file from the revision file. The whole process of using RCS is basically checking-in and checking-out the same file. RCS is not aware of time, so there is no need to check-in a text file when you have finished a programming session. You really only need to check-in a file when you have made all of the edits you need. If you are fixing a bug in a program, you would only check-in the file when you have fixed it. No need to keep track of working copies of the files. To show you how RCS is used, I've written a short Structured SuperBasic program: OPEN #3,con_100x100a100x100 CLS #3 INPUT "Enter The Name of a File :";file$ OPEN_IN #4,file$ REPeat loop INPUT #4,in$ IF EOF(#4) THEN EXIT loop PRINT #3,in$ END REPeat loop CLOSE #4 CLOSE #3 I stored this program in the file 'readfile_ssb'. I then checked it in to RCS using the following command: exec ci;"readfile_ssb" 'ci' then asks me for my initials. Since the QL does not have the concept of multiple users (and therefore user names), a persons initials are used to keep track of who has checked out a file and who has made what changes to that file. 'ci' then asks for a Description. The Description is where the "why" of the file change is kept track of. If you are using RCS to keep track of bug fixs for a program, the Description section would be used to mention the bug, what caused it, and how it was fixed. I do not believe there is a limit on how long the description can be. The Description is ended by a line with just a period on it. 'readfile_ssb' is now checked-in to the RCS revision file and is deleted. Now to get the file back to edit it, I ran the following command: exec co;"-l readfile_ssb" I need to tell 'co' what file I want, but I also have to tell 'co' that I want to edit the file. If I ran 'co' without the "-l" option, 'readfile_ssb' would have been created, but it would not have been "checked-out" from the revision file, and when I went to check it in, RCS would not have allowed it. Under UNIX, 'co' would create a 'read-only' copy of the file. 'co' asked for my initials. Since the same person who checks out a file is the only person the can check in a file, be sure to remember what initials you used. 'readfile_ssb' is now extracted from the RCS file and the revision file is changed to show that the file is checked out. Once I am done editing the file, I can then check in the file using 'ci'. Again I am asked for my initials. The file is now checked back into the revision file and deleted. If you copied the file before checking it back in and you have edited this file, you can not incorporate those changes into the revsion file since the file is 'officially' checked in and can not be edited. RCS uses revsion numbers to keep track of the different times a file is checked-in. The first revision is 1.1, then 1.2, then 1.3 and so on. Let's say that I have edited my file 4 times, I'm now at revision 1.5 and want to recall revision 1.3. I would run the following command: exec co;"-r 1.3 readfile_ssb" 'co' would then dig though the revision history and generate version 1.3 of 'readfile_ssb'. RCS comes with more than just the 'ci' and 'co' programs. 'rcsdiff' is used to compare a working "checked-out" file with the version still in the revision file. 'rlog' is used to view the revision file. Below is the output of 'rlog' from my example session: RCS file: readfile_ssb,v Working file: readfile_ssb head: 1.2 branch: locks: strict access list: symbolic names: comment leader: "# " keyword substitution: kv total revisions: 2;selected revisions: 2 description: Creation of RCS Archive ---------------------------- revision 1.2 date: 1998/06/11 20:41:01; author: tcs; state: Exp; lines: +2 -2 This is a second version of this program. ---------------------------- revision 1.1 date: 1998/06/11 20:19:34; author: tcs; state: Exp; Initial revision ============================================================ RCS is a lot more complicated that I've mentioned here. It, and SCCS, is fully documented in the book "Applying RCS and SCCS" by Bolinger & Bronson, published by O'Reilly & Associates. RCS for the QL is available from most freeware sources. The QL RCS distribution does not come with the 'diff' program which it requires. 'diff' is available as part of the C68 distribution. RCS can not be used with regular SuperBasic programming, as RCS would see any new line numbers as a change in the file. If you loaded a file into SuperBasic, did only a line renumber, saved it, and then check it back into RCS, RCS would any line with new line number as having changed. RCS works well with Structured SuperBasic as it has no line numbers. It also works well with C, Forth, Pascal, etc. ENVIRONMENT VARIABLES The concept of Environment Variables comes from the Unix world. They are used slightly in MS-DOS, but not at all to the same extent as UNIX. For the QDOS world, the file ENV_BIN provides a number of extensions that allow the use of Environment Variables. Essentially an environment variable is a variable that can be "seen" by exectuable programs. In SuperBasic we can set up all kinds of variables, but if we execute a program from SuperBasic, these programs can not "see" these variables and get data from them. The purpose of environment variables is to change the configuration of a program. They function like Config Blocks,but don't require running a program to make the change. Let's take a quick look at how we can change the behavior of programs. There are five different ways of doing this: 1] User Intervention. This is where the user uses a menu or answers queries from the program. 2] Config Blocks. This feature is pretty unique to QDOS, but it allows the user to change default options without having to know how to edit a binary file. 3] Configuration File. This is a separate file that the program reads to determine how to set defaults and run. 4] Command line Arguments. Instead of the program querying the user for information, the user types it in when they execute the program. 5] Environment Variables. The user sets a variable that is then read by the program and changes its default settings. Each of the options have their own place and their own benefits and faults. Some are more permanent, like Config Blocks and Config files, while some are very short lived, like User Intervention and Command line Arguments. Enviroment Variables are in between as they can be set in a BOOT program, but they can be changed by just typing in a new command. The ENV_BIN file comes with 4 extensions. They are: SETENV - Defines an environment variable. ENV_LIST - Lists all defined environment variables. ENV_DEL - Deletes an environment variable. GETENV$ - Gets the value of an environment variable. The two key commands are SETENV and GETENV$. SETENV is used like this: SETENV "VARIABLE=value" SETENV takes a string argument of the type "XXXXX=YYYYY" where XXXXX and YYYYY are two strings separated by an equal sign. Any space before the equal sign is treated as a part of XXXXX and any space after the equal sign is treated as a part of YYYYY. The case of each string is important as "VARIABLE" is different from "variable". By convention, upper case is used for variables. The SETENV is done either in a BOOT or setup program or by the user. The command for an executable to get the contents of an environment variable is GETENV$. The command is used like this: a$ = GETENV$("VARIABLE") In this case a$ will be assigned the value of "value". This comes from our previous SETENV command above. If the Environment Variable "VARIABLE" is not set (does not exist) then the GETENV$ would return a Null string ( "" ). Now I did say that executables use GETENV$ and not SuperBasic programs. Since variables are already used in SuperBasic, we would not gain much in using environment variables. There the commands are use is in compiled SuperBasic programs, which are executables. I see the purpose of using Environment Variables as adding to the flexibility of programs that use Config Blocks. Both Config Blocks and Environment Variables are really designed to change default program settings. User Intervention and Command Line Arguments are designed to tell the program what the data is. Using environment variables allows the user the ability of making a temporary change to the default options of a program, without having to go through the trouble of using "config". Environment variables would be used to change a setting for a single session and that's it. Getting Config Blocks and Environment Variables to work together is not difficult. The program would first get it's default settings from the Config Block. It would then check to see if there are any Environment Variables set. If there are, then the Environment Variable settings would override the Config Block settings. WORKING DIRECTORY As we all know, QDOS did not come with the concept of directories and subdirectories. A number of extensions to QDOS are available to help create the working concepts of directories. When using ED drives, I found the Path (PTH) extensions usefull because they would search a list of subdirectories looking for a particular exectuable. Now, I've never used a QL with a hard disk, so I'm not too experienced with using the tools for handling directories, plus my copies of some of the good articles dealing with this subject are not available to me. So, I've been doing some thinking on the matter, especially since I have been writing a freeware program and I want to make it as versatile as possible. The one issue that I've been pondering over is telling a program where there data file are at. This is called a Working Directory. It is a directory where the data files are at and any newly created files will also go. I know that DATA_USE is designed for this purpose, but I do not want to type in a new DATA_USE before each program I execute. I could set up a short little SuperBasic program to set up everything, but I want to EXEC a program not LRUN it. Of course, using a front end like QASCADE, this point is mute, since the user is removed from the details of executing the program. With PTH_ setup, I don't need to set PROG_USE, because PTH_ finds the executable. I was looking a way of having the executable know where the Working Directory is. What I came up with is this: Have an item in the Config Block for Working Directory. The contents would be something like this, "WIN1_PROG_DATA_". This string would be appended to the front of each file name used in the program. This does mean that the user would not be able to type in a new device name, such as "FLP1_FILE_EXT". Initally, the Config Block entry for Working Directory would be the null string (empty), so that a user would enter "FLP1_FILE_EXT". Once the user has created a working environment, the Config Block can be changed to what the user wants. If this feature is linked to Environment Variables, it would become more powerfull. By setting an Environment Variable for the program, the user could temporarily override the Config Block and change the Working Directory. If the Config Block was changed to a Working Directory and the user needed to use the floppy for a few times, the user would set the Environment Variable to "flp1_" or to the null string ("") and then set DATA_USE to flp1_. If they did not want to change the default Config Block, a BOOT program could set the Environment Variable and it would be active during the whole computing session. If different programs used different Environment Variable names for their Working Directory, then all Working Directories could be set up in the BOOT program. Something that could not be accomplished with just DATA_USE. Since my experience in this area is kind of light, the more experienced QLers may want to take this all with a grain of salt. I prefer to think of it as a way that I would prefer to organize things and others may have a different way. The reason I am bringing up this idea is for programmers to consider the concept and add it to their programs. The option is not very difficult to implement, but it would a nice feature to the program. The whole feature would take up no more than 10-20 extra lines of code to a program. By setting the default value of Working Directory to nothing, the feature is essentially turned off and it does not impede the user that does not want use a Working Directory. CASE STATEMENT IMPLEMTATION When I recently was working on SSB 2.6, I was using a SuperBasic implemtation of a CASE structure for the core of the program. The deeper the structure got, the harder it was to read and understand. I starting thinking of using another way of implementing a CASE structure. For those that don't know what a CASE structure or statement is, it is essentually the same structure as a SuperBasic SELECT ON statement, but it is not limited to numbers. Plus the general idea of a CASE structure is to have a number of possible logic statements with an action for all cases that do not fit one of the logic statements. In a generalized CASE structure not all of the comparisons must be based on the same data (string, number, etc), but can vary. Traditionaly, a way of creating the CASE structure is to have a number of nested IF..THEN..ELSE statements, with the final ELSE handling the default value. An example would be something like this: you are reading in text from a file. You want to handle the following cases: a line starts with a ##, or a line starts with a **, a line starts with a period (.). If none of these cases, then the line is passed to the output file. Using nested IF..THEN..ELSE statements the pseudo code would look something like this: IF line starts with ## THEN ...... ELSE IF line starts with ** THEN ...... ELSE IF line starts with a period THEN ....... ELSE pass to output file END IF END IF END IF If you start having more than just a hand full of possible cases, the structure can get long and difficult to read. SuperBasic allows the use of NEXT in conjunction with a REPEAT statement. This means that when the NEXT is reached, the rest of the REPEAT loop is skipped and the processing goes onto the next interation of the REPEAT loop. Using NEXT in this manner allows for the creation of a different implementation of a CASE structure. Below is an example of the two implementatios listed side-by-side. REPeat loop REPeat loop IF .... THEN IF .... THEN .... .... ELSE NEXT loop IF .... THEN END IF .... IF .... THEN ELSE .... IF .... THEN NEXT loop .... END IF ELSE IF .... THEN default .... END IF NEXT loop END IF END IF END IF default END REPeat loop END REPeat loop Using the REPEAT..NEXT..END version may not seem as clean as the "classical" IF..THEN..ELSE structure, but I think it is cleaner when it come to reading and debugging. What I am looking for from readers is to ponder over any downsides from using the REPEAT..NEXT structure. I can't think of any logical problems with using this structure over the classical one. If you know of any, please send me a note. I'll add the productive comments in the next issue. CREATING LOADABLE EXTENSIONS USING QLIB One of the things that has always amazed me about the QL was the ability to load a binary file and have a bunch of new keyboards available in SuperBasic. In most computers that had Basic built in, the language was static and had no way to extend itself. Other languages (like C, Fortran, or Pascal) used libraries of functions and procedures to extend the capability of the library. The first major loadable extention to the QL was ToolKit. From then on the term toolkit has been used in reference to loadable extensions. Popular toolkits are ToolKitII (TKII), DIY ToolKit, and DJToolKit. I knew that the first of these toolkits were written in Assembly, but I did not know that they could be created by Qliberator. It seems that QDOS executables and extensions are real close in format and when compiled right, they can be interchangable. This means that an executable can also be loaded as an extension. To figure out how to create a toolkit, I grabbed a simplefunction, Qliberator, and gave it a try. The function is included below: 10 REMark $$external 100 DEFine FuNction upper$(up$) 110 LOCal x, temp 120 FOR x = 1 TO LEN(up$) 130 temp = CODE(up$(x)) 140 IF temp > 96 AND temp < 123 THEN up$(x)=CHR$(temp-32) 150 NEXT x 160 RETurn up$ 170 END DEFine The function takes any string and converts it to all upper case letters. The $$external is a compiler directive to Qliberator that tells it that the next function or procedure needs to be available outside of the executable. For each procedure or function that you want to turn into an extension, you would have to put the $$external directive in front of it. If I was to put an additional line in the program, 180 PRINT upper$("This is a test") then when I executed the program, line 180 would be executed. If I LRESPRed the program, the keyword upper$ would be made available. When compiling the program it is a good idea to turn WINDS off, since the extension will have no channels open. Otherwise 3 channels will be opened for it, wasting them. To lower the size of the binary file, turning off NAMES and LINES might be a good idea. Note that the case of the function or procedure will be maintained in the extension. In my example, the name that will show up when entering EXTRAS is "upper$" (all lower case). If I defined the function a UPPER$, then "UPPER$" would show up in the EXTRAS command. By convention, extensions should be done in all upper case. If you will be running the extension on a system that already has the Qlib runtimes loaded, then compile the program without runtimes. If you don't know if the Qlib runtimes will be available, compile it with the runtimes included. It is a good idea to compile both ways and let the user decide which one they need. The example program when compiled without the Qlib runtimes was 594 bytes. With the Qlib runtimes it was 11,146 bytes. The runtimes take up a fair bit of space. If you load an extension that does not have the Qlib runtimes on a system where the Qlib runtimes are not loaded, you will not get an error message when you LRESPR the extension. When you call the extention is when the error will occur. The exact error message is: Error "Runtimes Missing !" Once you have compiled an extension, all that is needed is to LREPSR it and test it out. Remember that you can't LRESPR while any jobs, other than Job 0 (SuperBasic), is running.