RAS
Home
Welcome to RickSchummer.com
web site hosted by GoDaddy Software

(Version 4.5.19 - This site updated 02-Apr-2005)
 

What is new?

Favorites

Site Map

About RAS



United We Stand!

Blog

VFP Tools

VFP Books

VFP Announcements

Articles

Presentations

GLGDW 2001 Pictures

DevCon 2000 Pictures

Set Up Your Dot Com


Site Policies

Curriculum Vitae
Resume

Words of Wisdom

Track Space Station

Campgrounds

US States Visited

Browser Details

Photography Index

Biscuit Photos

Rocketry 2001 Photos


Older Information

News

RAS.Commentary

Great Truths

Websites of the Month

What was new (v3.x)

What was new (v2.x)

What was new (v1.x)


Getting Values From CONFIG.FPx

By Richard A. Schummer

Originally Published in the December 1995 issue of FoxTalk 

Copyrighted 1995 - Richard A. Schummer

Low level file input and output is probably the most under utilized feature within FoxPro. This article will demonstrate another use for these very powerful commands. I will demonstrate how to get value settings from CONFIG.FPW by parsing this file. Please note that the discussion will focus on CONFIG.FPW, but the features discussed in this article are appropriate for all FoxPro configuration files which include CONFIG.FP, CONFIG.FPW, CONFIG.FPM, and CONFIG.FPU.

Why Parse CONFIG.FPW

One might ask “Why would anybody want to do this?”. So first I will explain the scenario involved that caused me to write a routine that parses the configuration file. All my applications write error files when a fatal error occurs in the application (not very often mind you). These files have been written to the application’s default directory ever since I have written FoxPro applications. I can have these files viewed by the customer or faxed to me. These files help me debug the reason for the crash. This error trapping has allowed me to fix problem issues quickly and see what kind of problems are occurring that are not reported by my customers.

One of my customers recently changed network administrators. The new network administrator restructured file servers and refuses to have any files written to the application directory. What was I going to do? First I politely asked that these rarely written files be allowed on the server. This simple request was rejected. The network administrator wanted them written to the client PC, completely off the server. The application support team will have to go to the machine that the customer was using and collect my error files so they can start the debugging process. I feel an uprising by the support team will cause the network administrator to change his decision and put the files back on the server, but in a different directory. I decided to build in a little more flexibility to incorporate these possible change down the road.

In addition to accessing the files, the convenience of the error files being in the application directory was that I did not require a directory name to point to where these special files needed to be located. The files were written to the directory indicated by SET DEFAULT in CONFIG.FPW. CONFIG.FPW is opened by FoxPro (or the Runtime library) when it starts up. The default directory is set before the application starts so the error routine always had a designated place to write the files even if the program crashed on the line of code following the ON ERROR initialization. The directory name could have been stored in an application’s registration table, but what if the application crashed before the table was read? What if the reason the program was crashing was because the network administrator deleted the registration table? Using CONFIG.FPW provided an additional advantage of modifying the location of the files without needing FoxPro to modify a field in a table or having to provide this functionality within the application. The network administrators (or my customers on standalone PCs) could edit CONFIG.FPW to change where these files are written.

Lets Get to the Solution

I first thought about loading the entire configuration file into a memo field and parsing it from there. This possible solution was eliminated for the same basic reason that it was not include it in the applications registration table. I wanted to avoid having another table open or creating another cursor using the CREATE TABLE or CREATE CURSOR. I also had to use low level file commands to get the configuration file into the memo field anyway, so why not parse it directly.

The following solution is not rocket science. Here is a basic outline:

1. Use SYS(2019) to get the name of the CONFIG.FPW associated with the application (with full path).

2. Check to see that a configuration file is in use and the file is resident on the drive.

3. Open the CONFIG.FPW in read only mode.

4. Read in each line of CONFIG.FPW until the setting/variable you want is found.

5. Save the setting you are looking for in a memory variable.

6. Close the configuration file.

7. Return the setting to the calling program.

The program below is called CFGPARSE.PRG and is included on the Companion Disk. This program processes each of the steps outlined and incorporates some parameter checking logic. I will not discuss the parameter checking logic, just the parsing logic associated with the configuration file.

*************************************************

*

*             C F G P A R S E. P R G

*

*  AUTHOR: Richard A. Schummer     February 1995

*          CompuServe Id # 70254,1643

*

*  PROGRAM DESCRIPTION:

*     This procedure parses a FoxPro CONFIG.FPW

*     file to find a string setting.

*  CALLING SYNTAX: 

*     <variable> = CfgParse(cCfgFPVar, ;

*                           lQuotes, ;

*                           nNumRetry, ;

*                           lMultiInst, ;

*                           cSeparator)

*

*     Sample:

*     cGenScrnSet = CfgParse("_GENSCRN")

*

*  PARAMETERS:

*    cCfgFPVar  = CONFIG variable setting,

*                 ie., RESOURCE, or _GENSCRN.

*                 Character value, required 

*                 parameter.

*    lQuotes    = Do you want the value returned

*                 with quotes? Logical value, 

*                 not required, defaults to .T.

*    nNumRetry  = Number of times to try to open

*                 file before error occurs. 

*                 Numeric value, not required, 

*                 defaults to 1

*    lMultiInst = Should the routine look for 

*                 multiple instances of same 

*                 setting? Logical value, 

*                 not required, defaults to .F.

*    cSeparator = If there are multiple instances

*                 this is the string (character)

*                 which separates them. Character

*                 value, not required, defaults

*                 to one space.

*

*  RETURN VALUES:

*    .F.        = Problem with invalid or 

*                 missing parameter or could not

*                 get access to file

*    <string>   = Value in configuration file

*                 or null string is not found

*************************************************

 

PARAMETERS cCfgFPVar, ;

           lQuotes, ;

           nNumRetry ;

           lMultiInst, ;

           cSeparator

           

* The file name of CONFIG.FPW with full path

PRIVATE  cCfgFP

 

* Low level file handle of CONFIG.FPW file

PRIVATE  nCfgHandle 

 

* String read in by low level reads from 

* CONFIG.FPW

PRIVATE  cCfgStr 

 

* The directory name set by the cCfgFPVar 

* parameter in CONFIG.FPW

PRIVATE  cRetVal

 

DO CASE

   CASE PARAMETERS() < 1

     WAIT WINDOW "Please pass setting parameter!"

     RETURN .F.

   CASE PARAMETERS() < 2

     lQuotes    = .T.

     nNumRetry  = 1

     lMultiInst = .F.

     cSeparator = SPACE(1)

   CASE PARAMETERS() < 3

     nNumRetry  = 1

     lMultiInst = .F.

     cSeparator = SPACE(1)

   CASE PARAMETERS() < 4

     lMultiInst = .F.

     cSeparator = SPACE(1)

   CASE PARAMETERS() < 5

     cSeparator = SPACE(1)

ENDCASE

 

 

* Check if parameters are correct type

 

IF TYPE("cCfgFPVar") != "C"

  WAIT WINDOW "Setting/Value parameter not " + ;

              "character"

  RETURN .F.

ENDIF

 

IF TYPE("lQuotes") != "L"

  WAIT WINDOW "Quotes parameter not logical"

  RETURN .F.

ENDIF

 

IF TYPE("nNumRetry") != "N"

  WAIT WINDOW "Number of open retries " + ;

              "parameter not numeric"

  RETURN .F.

ENDIF

 

IF TYPE("lMultiInst") != "L"

  WAIT WINDOW "Multiple Instance parameter " + ;

              "not logical"

  RETURN .F.

ENDIF

IF TYPE("cSeparator") != "C"

  WAIT WINDOW "Separator parameter not character"

  RETURN .F.

ENDIF

 

* Read in CONFIG.FPW to get value of the

* variable setting.  The variable setting

* to be read is passed by parameter to the 

* procedure.

 

cCfgFP     = SYS(2019)

cCfgStr    = ""

nCfgHandle = -1

cRetVal    = ""

nRetry     = 0

 

* The number of seconds is included for testing

* purposes.  I tested the speed of the routine

* with buffering and no buffering.

 

nStartSecs = SECONDS()

 

* Only process if CONFIG file is in use

IF !EMPTY(cCfgFP)

 

  * Does file exist?

  IF FILE(EVALUATE("cCfgFP")) 

  

    * If so, open read only

    DO WHILE nCfgHandle < 0 AND nRetry < nNumRetry

      nCfgHandle = FOPEN(cCfgFP,0)

      nRetry     = nRetry + 1 

    ENDDO

    

    * File opened okay? 

    IF nCfgHandle < 0

      * Perform Error message routine

      DO ErrorCheckPR

      RETURN .F.

    ELSE

      DO WHILE !FEOF(nCfgHandle)

      

        * Read in up to a carriage return

        * (one line in CONFIG.FPW file)

        cCfgStr = FGETS(nCfgHandle)

           

        * If proper setting then remove any 

        * quotes, then make string to return

        IF UPPER(cCfgFPVar) $ UPPER(cCfgStr)

          cCfgStr = STRTRAN(cCfgStr,'"',"")

          cCfgStr = STRTRAN(cCfgStr,"'","")

          cRetVal = IIF(EMPTY(cRetVal), cRetVal,;

                        cRetVal+cSeparator) + ;

                    IIF(lQuotes, AddQuoteFN(),;

                                 GetCfgValueFN())

                                 

          IF !lMultiInst

            EXIT

          ENDIF

        ENDIF  

      ENDDO

     

      * Close CONFIG.FPW

      =FCLOSE(nCfgHandle)

    ENDIF

  ENDIF

ENDIF

 

nEndSecs   = SECONDS()

 

* Test WAIT WINDOW to determine access speed of

* the parsing routine

 

* WAIT WINDOW STR(nEndSecs - nStartSecs, 10,5)

 

 

RETURN cRetVal

 

*************************************************

* Function that adds quotes to the returned value

*

* No Parameters

*************************************************

FUNCTION  AddQuoteFN

PRIVATE cRetVal    && Value returned from func

cRetVal = "'" + GetCfgValueFN() + "'"

RETURN cRetVal

 

*************************************************

* Function that determines the value to be 

* returned from the CONFIG.FPW file

*

* No Parameters

*************************************************

FUNCTION  GetCfgValueFN

PRIVATE cRetVal    && Value returned from func

cRetVal = ;

  ALLTRIM(SUBSTR(cCfgStr,(RAT("=",cCfgStr)+1)))

RETURN cRetVal

 

*************************************************

* Provide low level error checking for opening

* configuration file

*

* No Parameters

*************************************************

 

PROCEDURE ErrorCheckPR

 

PRIVATE cMsg       && Msg displayed to user

 

cMsg = ""          && Initialize to no message

 

DO CASE            && Determine which error

   CASE FERROR() = 0

     cMsg = ""

   CASE FERROR() = 2

     cMsg = "File not found"

   CASE FERROR() = 4

     cMsg = "Too many files open"

   CASE FERROR() = 5

     cMsg = "Access denied"

   CASE FERROR() = 6

     cMsg = "Invalid file handle given"

   CASE FERROR() = 8

     cMsg = "Out of memory"

   CASE FERROR() = 31

     cMsg = "General opening file failure"

ENDCASE

IF !EMPTY(cMsg)

   WAIT WINDOW "Cannot open "+cCfgFP+": " + cMsg

ENDIF

RETURN

The first and second steps are easy. Store the configuration file directory and name to a memory variable and check for residency on the disk.

cCfgFP = SYS(2019)

IF !EMPTY(cCfgFP)

  IF FILE(EVALUATE("cCfgFP"))

    {rest of code here}

  ENDIF

ENDIF

The third step opens the CONFIG.FPW in read only mode. To open the configuration file use the FOPEN() function.

nCfgHandle = FOPEN(cCfgFP,0)

The first parameter in FOPEN() is the file name, followed by the opening mode parameter. The following table defines the different modes an existing file can be opened in:

Parameter Mode Buffering
0 Read Only (default) Buffered
1 Write Only Buffered
2 Read and Write Buffered
10 Read Only Unbuffered
11 Write Only Unbuffered
12 Read and Write Unbuffered

The value returned is the file handle associated with the configuration file just opened. FOPEN() will return a negative one (-1) if there was a problem opening the file. I ran into a problem while testing this routine on a standalone PC. If I already had the configuration file open with a different handle from the command window, then the program could not open the file. The FERROR() function was reporting “Access Denied” (see sidebar on the FERROR()). This was not making sense since both files were open with the read-only option. Multiple instances of FoxPro can share the file with no problems, but the same instance of FoxPro cannot open the same file again with low level file functions. This was confirmed by Microsoft’s Technical Support. I inserted some logic to retry opening the configuration file for a given number of times. The routine is extremely fast, the actual contention time for the file is minimal. I am not sure why a developer would want to open the CONFIG.FPW file again within the same application, but a few years ago I wasn’t sure why Microsoft included the USE...AGAIN feature when opening tables. Today I use this feature all the time.

I opened the CONFIG.FPW file with the buffered option. Some informal tests showed that file processing took eight times longer when unbuffered file access was used.

The next few lines of code read in each line of CONFIG.FPW until the setting/variable you want is found. We are all use to DO WHILE NOT EOF() loops as we process through FoxPro tables. The equivalent to process through a table opened with low level file functions is DO WHILE NOT FEOF(<file handle>).

Each line in the configuration file is read by the FGETS(<file handle>) function. FGETS() will read up to 254 bytes of the file, or to a carriage return character. You can also specify the number of bytes you want to read in the optional second character. I selected to use the default characteristics of the FGETS() function so a full line in the configuration file would be read. After each line read from the CONFIG.FPW file, the routine checks to see if the search string is in the line. If it is the routine calls the extraction logic to get the value after the equals sign. The routine replaces all quotation marks with null characters so FoxPro is not confused with “strings within strings”. The lQuotes parameter allows the developers to select if the value is returned with quotes or not. This literal value is the value returned to the calling program.

I also added some logic to allow the program to find multiple instances of the settings/variable.  This allows the developer to look for GenScrnX drivers that happen on the same hook. For instance a sample CONFIG.FPW could look like this:

RESOURCE = C:\FOXCFG\FP26WIN\FOXUSER.DBF

TMPFILES = C:\TEMP\

DEFAULT  = C:\DEVWAPPS\

KEYCOMP  = WINDOWS

MVCOUNT  = 512

_GENSCRN = C:\FOXADDON\GENSCRNX\GENSCRNX.PRG

_GENMENU = C:\FOXADDON\GENMENUX\GENMENUX.PRG

_SCXDRV5 = "C:\FOXADDON\GENSCRNX\3D.PRG"

_SCXDRV5 = "C:\FOXADDON\INTLTOOL\INTL.PRG"

_INTLUPDATE=C:\FOXADDON\INTLTOOL\

If I wanted to find out what GenScrnX drivers I had associated with the _SCXDRV5 hook I could call the CFGPARSE.PRG as follows:

cVal=CfgParse("_SCXDRV5",.F.,100,.T.,SPACE(1))

The Real Life Example

I needed to implement this concept in the beginning of my application’s main program. The call to the configuration file parsing routine needs to be before the ON ERROR call so I can tell the ON ERROR routine where the files go. The following code is how I implemented the error file location logic:

* Variable in CONFIG.FPW with error

* files directory, ie

* _ERRFILES = P:\DATA\TEMP\

PUBLIC  cErrVarFP

PUBLIC  cErrFiles

cErrVarFP = "_ERRFILES"

* Get Error file location

cErrFiles = CfgParse(cErrVarFP,.T.)

* Check for problem accessing file

* or with parameter type problem.

* Message was displayed in CfgParse

IF TYPE("cErrFiles") = "L"

  cErrFiles = ""

ENDIF

ON ERROR DO ErrHandl.fxp  WITH ERROR(),;

                               MESSAGE(),;

                               MESSAGE(1),;

                               PROGRAM(),;

                               LINENO(),;

                               cErrFiles

The cErrVarFP allows my teammates to switch variables based on the needs of the application. I want the quotes included whether the network administrator includes them in the configuration file or not so my error handler processes the variable correctly. The error handler also handles trailing “\” at the end of the directory name. Now the network administrators can put the error files any where they choose to. My only request is easy access so I can view them if my application decides to crash (like it has a mind of its own).

This same concept could be used to tell a program where the database tables are located. I have traditionally stored this information in a data dictionary and registration tables, but this has caused problems when the users decided to locate the tables in another directory. I built in functionality into the application that allowed them to make change necessary to find these tables. I think it would be easier to use an editor to modify a setting in CONFIG.FPW for the same reason as the error files.

Issues of Concern

Shared access on a client PC machine works great if there are multiple instances of FoxPro, or multiple instances of a runtime applications. There is no file share attribute settings within DOS. This is handled by SHARE.EXE or VSHARE.386. This feature is nice in case you have a couple of FoxPro applications running at the same time, both sharing one copy of the CONFIG.FPW file and the runtime library.

File sharing over a network for multi-user access is critical. The network allows the low level FOPEN() function to reopen and share files that are flagged as sharable. The low level functions do cooperate and allow multiple users read-only access to the files.

If you have any lines in the CONFIG.FPW file that exceed the 254 character limit of FGETS(), then you will get erroneous results. The onus is on the developer to make sure this is not the case. It is possible to use the FGETS() or FREAD() functions and build in the necessary code to handle lines past the 254 character boundary. I did not need this functionality, so I did not build it in.

I also did not protect the code from not including the equal sign as a delimiter between the setting/variable and the value. Again, the onus is on the developer, the equal sign is expected to be there. This is consistent syntax with the rest of the FoxPro settings.

Conclusion

The implementation was fairly easy. The hardest part is remembering all the syntax for the low level file commands (thank goodness for on-line documentation).

Like most features built into FoxPro, the low level file functions are lightning fast. The parsing routine described in this article only needs to work on a small file. Other routines I have written with low level file functions with large amounts of data have run very fast as well.

This was a simple example of low level functions usage that provides a elegant solution to what started out as a sticky situation. This same philosophy could be applied to other system files such as AUTOEXEC.BAT, CONFIG.SYS, WIN.INI, or SYSTEM.INI.

 

SIDEBAR for FERROR()

The FERROR() function provides the FoxPro developer with detailed information about the previous low level file error condition. This function returns the error number. It is then up to the developer to handle the error appropriately. The following table contains the different error numbers and messages that correspond.

Error Number Message returned
2 File not found
4 Too many files open (out of file handles)
5 Access denied
6 Invalid file handle given
8 Out of memory
25 Seek error (can't seek before the start of a file)
29 Disk full
31 Error opening file

The ErrorCheckPR procedure in the CFGPARSE.PRG is an example on how low level file errors can be handled. You will notice that not all the errors were accounted for in this routine. The program is only opening, reading, and closing the file. Therefore there is no need to handle errors for SEEKs (25), and Disk Full (29). It is a real good idea to process error checks if the low level file function does not return an expected value.

This site is designed to be viewed in 800x600 video resolution or higher, but it should work fairly well at any resolution and with any of the major browsers (all free!). Optimized for MS Internet Explorer, Firefox, and Opera (mostly works with Mozilla and Netscape Navigator).

Copyrighted 1998-2005 Richard A. Schummer
All Rights Reserved Worldwide
This page was last updated on 02-Apr-2005