Some of the techniques can also be used to work with files of any content; however, this article will concentrate on text-files.
When do you need to manipulate text-file?
There may be several cases where you need to create or read text-files (or binary files, in some cases), for instance:
Note that for interfacing with other programs, importing and exporting to text-files or binary files is not the only option. Other options, for instance, XML files (which are actually special text-files), ODBC and Automation, are beyond the scope of this article.
Standard import and export commands
The standard commands for importing and exporting text-files are APPEND FROM (for import) and COPY TO (for export). They are relatively easy to use, in that they don't require you to loop through the records - these commands can can process an entire table at once. The fact that you use a single command also makes these commands very fast.
Two common text-formats are supported. With a comma-delimited text-file, each field is separated from the next with a comma (other characters are supported, too). With a fixed-width text-file, each field is identified solely by its position. For instance, the first 10 bytes in each record might correspond to the first field, the next 3 bytes to the second field, etc. In both cases, records are separated by the standard new-line symbol (carriage-return and line-feed, that is, chr(13) + chr(10)). The corresponding keywords are DELIMITED and SDF.
For exporting to a text-file, just select a table or prepare a cursor, and use a command similar to:
copy to MyText.txt type SDF * (or: type delimited)
For importing a text-file into a table, you need to prepare a table or cursor with the correct structure first.
These commands are very appropriate for a quick import or export, but if your text-data has (or requires) a very specific format, that can't be handled by these two standard text-formats, you may need some of the additional commands, to which I dedicate the remainder of this article.
Standard report in text-mode
A standard VFP report can be output as a text-file:
report form TestReport to file output.txt ascii
The variables _asciicols and _asciirows specify how many columns and lines you want on a page. Specifically, _asciicols will alter the horizontal spacing between elements on an existing report.
However, I found it very hard to have an exact control over the placement of individual objects; basically, this requires trial-and-error. Also, if you later happen to need a wider report than original planned (increasing _asciicols), you have to redo the element spacing all over again. Therefore, I don't recommend this option.
An alternative that makes it easier to control the placement of objects is to create and maintain the report with FoxPro/DOS, and then run it in Visual FoxPro, without converting it.
FileToStr(), StrToFile(), alines() and mline()
In VFP 6 or later, you can use FileToStr() and StrToFile() to copy quickly from a file on disk to a memory variable, or the other way round.
The following sample creates a text-file with ten lines:
erase output.txt for i = 1 to 10 StrToFile("Line " + trans(i) + chr(13) + chr(10), "output.txt", .T.) next
Note that if the third parameter is .T., the output will be appended to the file, instead of overwriting the entire file.
For importing data, the alines() function is especially easy to use, since it converts each line into an array element.
The following example will show the contents of the autoexec.bat file, adding line numbers to the left:
for i = 1 to alines(laMyText, FileToStr("c:\autoexec.bat")) ? transform(i, "###"), laMyText(i) next
Manipulation with alines() is very fast, too. However, unfortunately, the VFP limitation of 65,000 elements per array translate to a maximum of 65,000 lines of text per file.
If you currently use more lines, or want to plan for future growth, you can use the mline() function instead. The help file states that it is designed for memo fields, but it works very well for long character expressions in memory, too.
I will now repeat the previous example, using the mline() function. Since the idea is to use this sort of code on possibly large files, the sample is optimized for speed in two ways: 1) I used the _mline variable, as explained in the VFP help. 2) I abstained from counting the total number of lines beforehand, with memlines(). The first point is especially important. For instance, if you use mline() to return line #1000 directly, VFP must first search for end-of-line characters for the first 999 lines, in order to find the 1000th. line.
set memowidth to 200 lcMyText = FileToStr("c:\autoexec.bat") _mline = 0 i = 0 FileSize = len(lcMyText) do while _mline < FileSize - 1 i = i + 1 ? transform(i, "###"), mline(lcMyText, 1, _mline) enddo
This will solve the alines() limitation of maximum 65,000 array elements. However, reading a large file into RAM in one single piece can be very slow, if the file is larger than the free RAM (this will usually work, but the use of virtual memory can slow things down considerably). If this is the case, I would recommend using low-level file-functions instead.
Low-level file-functions
Low-level file-functions (LLFF) give you the ultimate control over each and every byte you import or export. First, as an example, let's repeat the example of showing the autoexec.bat file, this time with LLFF:
lnFile = fopen("c:\autoexec.bat") if lnFile < 0 MessageBox("Error opening file.") else i = 0 do while not feof(lnFile) i = i + 1 ? transform(i, "###"), fgets(lnFile) enddo fclose(lnFile) endif
Since LLFF are a little more involved than the previously explained methods, I will try to explain the individual steps.
First, an existing file is opened with fopen(), or (for the export case) a new file is created with fcreate().
These functions return a "file handle", a number which has to be stored into a variable, in order to access the same file later in the program. If the file can be opened or created successfully, a positive number is returned. A -1 indicates some problem. (Note that I didn't include error handling in the other examples - I had assumed the file existed.)
An entire line can be read from the file thus opened, with the fgets() function. For output, an entire line can be written with fputs(). If you need to work with individual bytes, you can use fread() and fwrite() instead.
For importing data, you will usually want to continue reading data until you reach the end of the file, which you detect with feof(). After you do all required processing, whether you opened an existing file or created a new file, it has to be explicitly closed, with fclose().
For completeness sake, here is a sample code for the export case. This program will create a short text-file, using LLFF:
lnFile = fcreate("output.txt") if lnFile < 0 MessageBox("Error creating file.") else fputs(lnFile, "Line 1") fputs(lnFile, "Line 2") fclose(lnFile) endif
Output with ? and ??
The ? and ?? commands (as well as the commands in the next two sections) are for the export case only, that is, to create text-files.
Both the ? and the ?? commands will output the expressions specified. If you specify more than one expression, separated with commas, they are automatically separated by a space. However, separate ? commands will not add this space.
?? will add a new line to the output, ? will not. Note that unlike similar commands in other languages, the line is added before the rest of the output specified in the command. By default, the output goes to the screen.
Now, let's see a short sample, how to produce a text-file with these commands. For illustration purposes, I separated the code to produce the second line into two separate commands.
* Setup code set console off set alternate to output.txt set alternate on * Produce the output ?? "This is line 1" ? "This", "is" ?? " line", "2" * Cleanup code set alternate off set alternate to set console on
Once you SET ALTERNATE to a file, output of several other commands, like LIST, LIST STRUCTURE and DIR, will also go to your text-file.
Output with @...SAY
The @...SAY command, which also prints to the screen by default, is another option to create text-files.
This command lets you output to specific coordinates of your text-file immediately. In other words, you can specify a line or column number directly, without explicitly outputting empty lines or columns. However, output has to be produced in ascending order. VFP treats the output as if it were a printer, which means that going back to a previous line will insert a page-break.
set device to file output.txt @ 0, 10 say "This is the title" @ 3, 0 say "Some text" @ 2, 0 say "This would go to the next page" set device to screen
Visual FoxPro keeps track of row and column positions; these can be accessed with the row() and col() functions; or you can just print relatively to the current position with the special $ operator instead of the number coordinates:
set device to file output.txt @ 1, 0 say "Hello" @ $, $+2 say "World" @ $+3, 0 say "Some more text" set device to screen
Unfortunately, the help topic for this versatile command only says it is "included for backward compatibility". For more details, you will therefore have to get the help file for FoxPro 2.x.
Outputting with TEXTMERGE
Another alternative for outputting text files is to use the TEXT and SET TEXTMERGE commands. You can either output lines of text with \ and \\, or, to avoid the repeated use of these symbols on several consecutive lines, between TEXT and ENDTEXT. The following sample illustrates how this is done:
set textmerge on to output.txt noshow \\Line 1 \Line 2
text Line 3 Line 5 Today is <<cdow(date())>>, <<date()>>. Your hard disk has <<transform(diskspace(), "###,###,###,###")>> bytes free. endtext set textmerge to
Note that when you combine constant text with variables or expressions, your code is much more readable with these commands, than with the other alternatives shown.
Handling binary files
All the previous examples are for text-files. You can use some of the options for binary files, that is, files in any format. For instance, ? and ?? can output any expression, not just plain text; StrToFile() and FileToStr() can work with both text and binary data, and the low-level file functions can work both on entire lines of text, and on individual bytes.
The real problem with binary files, of course, is that a binary format may be quite complicated, and it may be difficult to get documentation on the details of how some file types are stored.
Printing
If you want to print your text-file in text-mode, you can use the "???" command to send printer codes, followed by your text-file, directly to the printer. No interpretation is done by the Windows printing system - even a page-break has to be added explicitly (check your printer manual - for standard Epson dot-matrix printers, it is chr(12)).
Also, Universal Thread download #9957, by the late Ed Rauh, is a class specifically designed to bypass the Windows printing system, and print directly to the printer.
I have created a class to take care of many of the repetitive details of creating text-mode reports (like programming the main loop and taking care of break expressions, among others). It is available as Universal Thread download #9991. Other text-report downloads are available as well, but I haven't tested them thoroughly.
Conclusion
Visual FoxPro offers you several alternatives to access text-files. It is very likely that sooner or later, you will need some of them, for the purposes outlined at the beginning of this article.