The dBASE File Object

Last Modified: January 31, 2001
Ken Mayer, Senior SQA Engineer
dBASE, Inc.

Example files available in fileobj.zip


NOTE: This document was originally written for Visual dBASE 7.0/7.01, it has been updated for dB2K (release 1) and later versions of dBASE to include information about new properties, events, etc., and any new controls since the document was first written. If a control is new it will be noted. In updating this document, the images were left "as is" unless it was felt to be absolutely necessary to change them ...

In addition, this document refers to dB2K a lot, but unless it is about a dB2K specific aspect, the text can be used for Visual dBASE 7.0 through Visual dBASE 7.5.


The Purpose of this Document

This document will attempt to explain the basics of using the File Object that is part of dBASE to perform a variety of tasks - this is what is sometimes called "low-level file handling" -- meaning that you are opening a file (either a text/ascii/ansi file or a binary file) and manipulating it directly.

Don't let the term "low-level file handling" scare you. The file object is not complicated or hard to use. This is not programming in assembler or working with dBASE's internals.

This kind of processing can be read-only, read/write, write-only, append, etc. There are a lot of options and features here, and this HOW TO document will not spend a LOT of time on all of them.

If you wish to manipulate binary files, you may want to obtain the dBASE Users' Function Library Project (dUFLP) in the Knowledgebase, and examine the BFILE.CC class -- this is a subclass of the file object that adds some functionality specifically for binary files.

If you have used the low-level file functions in earlier versions of dBASE, you will find that those functions map directly to methods in the file class.


Properties and Methods of the File Object

Before we do anything else, let's look at the properties and methods of the file object.


The Basics ...

Creating an Instance of a File Object
In order to use the file object, you need to create an instance of the object:

   fMyFile = new File()

Now you have a file object, but there is no file open or being created ...

To Copy a File
Copying a file can be done in dB2K in several ways, including using the "COPY FILE" command. If you have a file object already instantiated, however, you can copy a file simply by using:

   fMyFile.copy( "somefile.ext", "somefile.bak" )

You can use memory variables to represent files, and you can even use wildcards (i.e., "*.TXT" ) -- if you use wildcards, dBASE will open the "getFile()" dialog.

If you store files in your executable (some developers like to "hide" files in the .EXE -- this keeps the user from being able to mess with them), the copy() method will look for the file in the .EXE itself, and copy it out to disk from there. You cannot, however, copy files into the .EXE.

If SAFETY is ON, dBASE will ask if you want to over write a file using this method.

If you use this method to copy tables, the auxillary files (.DBT, .MDX, etc.) are not automatically copied. You are better off using the copyTable() method of the database object.

If a file is open for writing (i.e., it is not opened using the "R" attribute) you cannot copy it until it has been closed.

Rename a File Renaming a file works very much like copying a file, and the same comments noted above under the discussion of copying a file will hold true (you can't rename a file that is open, etc.).

   fMyFile.rename( "somefile.ext", "somefile.bak" )

Deleting a File Deleting a file is very straightforward:

   fMyFile.delete( "somefile.ext" )

However, there is an optional parameter that allows you to send a file directly to the recycle bin (if you do not use this, it will not go to the recycle bin, and will not be recoverable through that means). If you add "true" as a second parameter (see below), and if SAFETY is set to ON, you will be asked if the file should be sent to the recycle bin.

WARNING: If you do not use the recycle parameter (or if it is set to false), you (or your users) will not be asked about deleting a file, whether or not SAFETY is ON or OFF (i.e., SAFETY is ignored ...).

   fMyFile.delete( "somefile.exe", true )

This method cannot delete an open file, and will not automatically delete auxillary files for tables -- if you need to delete a table you should use the database object's dropTable() method.

See If a File Exists
You can check to see if a file exists easily enough:

   fMyFile.exists( "somefile.ext" )

This is the same as the dBASE "FILE()" function.

To Check Attributes of a File
You can determine various file attributes using the file class, most of these deal with dates and times, but not all:

The accessDate() method returns the last date that a file was opened for read or write purposes.

The createDate() method returns the date the file was created.

The createTime() method returns the time on the date the file was created.

The date() method returns the last date the file was modified.

The shortName() method returns the DOS 8.3 character filename. You can use longer filenames in Windows 95 through NT versions. However, the operating system still tracks a shorter filename for DOS, and in case the file needs to be used on an older operating system. You can find out what this is using this method.

The size() method returns the size of a file in bytes (characters).

The time() method returns the last time the file was modified on the date it was last modified.

Opening or Creating a File
Once you have an instance of a file object, you can then either create a new file with the create() method, or you can open an existing file with the open() method.

In either case, you need to determine the "access rights" for that file. Your options are:

R Read-only text file
W Write-only text file
A Append-only text file
RW Read and write text file
RA Read and append text file
RB Read-only binary file
WB Write-only binary file
AB Append-only binary file
RWB Read and write binary file
RAB Read and append binary file

The access rights string is not case sensitive, and the characters can be in any sequence (i.e., "RW" or "WR" ...).

As soon as you open the file, the handle property of the file object will change from "-1" to some numeric value representing the file handle. You don't really have to worry about tracking the file handle (you used to if you used the low-level file functions) as the file object takes care of it for you.

A simple example would be to create or open a log file, and store the current date and time to the file (this is taken from online help ...):

   LOG_FILE = "AccessLog.txt"
   f = new File()

   if f.exists( LOG_FILE )
      f.open( LOG_FILE, "A" )
   else
      f.create( LOG_FILE, "A" )
   endif
   f.puts( new Date().toLocaleString() )
   f.close()   

Basically this code would check to see if the log file exists, and if so, open it, if not create it. In both cases it only opens the file with "Append" rights (add data to the end). It stores the current date and time using the puts method, and then closes the file.

Looping Through a File
You can loop through a file that you have opened as long as it is opened with read access writes. This can be handy if you need to skim through the file to find a specific item, or do some processing on the file.

The important thing is that you should always make sure you do not process past the end of the file. This is done by checking the value returned by the eof method:

   f = new file()
   f.open( "ATextFile.TXT", "R" )
   do while not f.eof()
      ? f.readLn()
   enddo
   f.close()
The above would loop through a file, displaying each line.

You might want to, instead, store the value returned by the readLn() method to a variable so that you could parse it (scan it for some text, that kind of thing) -- in which case the code might look like:

   f = new file()
   f.open( "ATextFile.TXT", "R" )
   cLookFor = "Something to find"
   do while not f.eof()
      cLine = f.readLn()
      if cLookFor $ cLine
         // do something
         ? "Found it!"
         exit
      endif
   enddo
   f.close()

The code shown above would loop until it finds a specific character string in the line extracted from the file, and if it does find it, it would exit the loop. Otherwise it would continue until the item being looked for was found, or the end of file was reached.

Reading From A File
There are two methods of reading data from a file, the first we have already used, which is readLn() (gets() is identical to readLn()), the other is read(). The primary difference between them is that readLn() (and readLn()) will read a line of text, which means it must end with standard "end of line" characters (Carriage Return/Line Feed).

You can, with the readLn() method, specify the number of characters to read, as well as an optional End of Line character to look for, but most of the time if you are using readLn() you will want to just read a line at a time. If the readLn() method does not find an end of line character it will read until it reaches the end of the document and return everything from where it started to that point (the maximum size of a character string in dBASE is, according to online help, approximately 1 billion characters, if you have enough virtual memory).

The read() method will read exactly the number of characters you tell it to. However, and more importantly, if you are working with a file that is not a text file (i.e., a binary file) you will want to use the read() method.

Writing To A File
There are two methods to write to a file: write() and writeLn() (the puts() method is identical to writeLn()).

The major difference between these two methods is that writeLn() adds the EndOfLine character to the end of the line, and write() does not. You would use write() to overwrite information in a file, or to work with binary files. If you wish to write a line at a time to your output file, then you would use the writeLn() method.

Both methods return the number of characters that were written to the file.

Examples
(These examples are based on examples in LOWLEVEL.HOW by Romain Strieff, which were written for Visual dBASE 5.5 and the low-level file functions ...)

Example 1 - Strip Blank Lines From Text File
The first example will read a text file and strip blank lines out, by reading it one line at a time, and writing every line that isn't blank to a copy of the file -- for this, the gets() and writeln() methods are the best way to handle it (the following code is attached to this HOW TO document as "STRIPBLANKS.PRG"):

   n=0                               // counter for blank lines
   fReadIt = new File()
   fReadIt.Open( "hasblanks.txt", "R" ) // open the file as read-only
   fWriteIt = new File()
   fWriteIt.create( "noblanks.txt" )    // create new file

   do while not fReadIt.eof()        // loop
      cString = fReadIt.readLn()     // read a line
      if not empty( cString )        // if not empty
         fWriteIt.writeLn( cString ) // write line to file
      else
         n++                         // increment blank line counter
      endif
   enddo
   fReadIt.close()                   // always close your files!
   fWriteIt.close()
	msgbox( n + " empty lines removed!" )

If you open the file "noblanks.txt" there should be no blank lines in it.

Example 2 - Changing "Unix" Style File to DOS/Windows Style
A textfile is normally formatted like this:

    -----------------------------------------------------------
    This is the first line of this file.CR/LF
    This is the second line of this file.CR/LF
    -----------------------------------------------------------

A LINE is by definition terminated by CR/LF in DOS/WINDOWS. This is not always the case in other operating systems (like UNIX).

Unless you are operating on very special textfiles with paragraphs exceeding the maximum possible line length (which in dB2K is huge), readLn() is the way to go.

The following example will be using readLn() to read lines of text out of a text file that is known to be a "Unix" file, and write out to a different file using writeln() (which automatically writes the CR/LF characters to the file).

Note that the following code is in the file "fromUnix.prg" that came with this HOW TO document, as is the file "readme.unx" -- which had the carriage returns stripped for me by Gary White.

An important note on the first use of the readLn() method is that if you do not specify some number of characters to read, and the file needs to be "translated", then the readLn() method will attempt to read the whole file. In the case of some files, this is not an issue, but if you are trying to convert a six megabyte file, you will run out memory (I know -- I tried!).

   fReadIt = new File()
   fReadIt.Open( "readme.unx", "R" )
   fWriteIt = new File()
   fWriteIt.Create( "readme.dos", "A" )

   // Check to see how long the first line
   // of the file is:
   cString = fReadIt.readLn( 1000 )
   nLength = cString.length
   fReadIt.seek( 0 ) // back to the top of the file

   // if the first line is wider than 80 characters,
   // we need to convert this file ...
   nLines = 0
   if nLength > 80 
      do while not fReadIt.eof()          // loop
         // 10000 = an arbitrarily large value for the 
         // number of characters to read
         // chr( 0x0A ) = the Line Feed character -- information
         // for these is in online help
         cString = fReadIt.readLn( 10000, chr( 0x0A ) )
                              // read a line to the Line Feed
                              // this places the position pointer
                              // on the next line ...
         // the writeLn method writes out the CR/LF automatically:
         fWriteIt.writeLn( cString )
         nLines++
      enddo
   else
      ? "No need to convert the file!"
   endif
   fReadIt.close()
   fWriteIt.close()
   ? nLines+" lines converted to file 'readme.dos'"

Example 3 - "APPEND FROM" a File With Non-Standard Separators
One of the reasons I am somewhat familiar with low-level file handling is that I routinely receive (every couple of months) a fairly large file that uses a pipe (|) symbol as the separator between the fields.

This is a bit frustrating, to put it mildly, since dB2K's "APPEND FROM" command does not have an option to use that symbol.

The data I normally receive is a lot more complex than what we'll look at here, but consider the following:

This can get a bit tricky to parse out.

The heavily documented code below will create the table, read the text file line-by-line, parse out the fields, and add new rows to the table, saving the appropriate fields ...

The program file "TOTABLE.PRG" came with this HOW TO document, as well as the sample file "somedata.txt".

   // Delete table if it exists:
   if file( "testconvert.dbf" )
      drop table testconvert
   endif

   // Create table:
   create table "testConvert" ( ;
       "testConvert"."First Name" character( 20 ),;
       "testConvert"."Last Name"  character( 20 ),;
       "testConvert"."Address1"   character( 30 ),;
       "testConvert"."Address2"   character( 30 ),;
       "testConvert"."City"       character( 20 ),;
       "testConvert"."State"      character( 2 ),;
       "testConvert"."PostalCode" character( 10 ),;
       "testConvert"."Phone"      character( 14 ) )

   // instantiate file object:
   f = new File()
   f.open( "somedata.txt", "R" )

   // instantiate query:
   q = new Query()
   q.sql = "select * from testConvert"
   q.active := true

   // shortcut to fields array:
   fFields = q.rowset.fields

   // start the loop:
   nRows = 0
   do while not f.eof()

      // read a line:
      cLine = f.gets()
      q.rowset.beginAppend()

      // start parsing ...
      cString = ""
      nPipe = 0 // number of pipes found
      for i = 1 to cLine.length

          cChar = cLine.substring( i-1, i ) // current character
          // if it's not a pipe symbol and we're not at the
          // end of the line (the last field ...)
          if ( cChar # "|" ) and ( i < cLine.length ) 
             cString += cChar // add current character to string
          else // it IS a pipe symbol or last character in line
             if cChar # "|"      // it's the last character in the line
                cString += cChar // add to end of field
             endif
             nPipe++ 
             fFields[ nPipe ].value := cString
             cString = "" // blank it out again
          endif

      next // end of parsing

      // save the row:
      q.rowset.save()
      nRows++
   enddo
   f.close()
   q.active := false

   ? nRows+" rows saved to table"

It's very surprising how fast this is ... granted, the test file only has four lines/rows, but still ...


Summary

A lot of the methods that can be used with the file object in dB2K are not discussed here. Some of them just look at files to obtain information, and others are complex enough that it would take pages and pages to try to explain.

There are some low-level file functions in the dBASE Users' Function Library Project (dUFLP) in the Knowledgebase in the files FILEEX.CC, BFILE.CC, and DBF7FILE.CC -- the first one does not use the file object, but rather the low-level file functions in dBASE. The other two are subclassed from the file object. You may want to take a look at these as well ...


DISCLAIMER: the author is an employee of dBASE, Inc., but has written this on his own time. If you have questions regarding this .HOW document, or about dB2K you can communicate directly with the author and dBVIPS in the appropriate newsgroups on the internet.

.HOW files are created as a free service by members of dBVIPS and dBASE, Inc. employees to help users learn to use dB2K more effectively. They are edited by both dBVIPS members and dBASE, Inc. Technical Support (to ensure quality). This .HOW file MAY NOT BE POSTED ELSEWHERE without the explicit permission of the author, who retains all rights to the document.

Copyright 2001, Kenneth J. Mayer. All rights reserved.

Information about dBASE, Inc. can be found at:

       http://www.dbase.com
    

EoHT: FILEOBJECT.HTM -- January 31, 2001 -- KJM