ERights Home elang / io 
Back to: URI Expressions No Next Sibling

Text File I/O


Pay no attention to the man behind the curtain.

(The following lines of code set up your file system so that this test will succeed. It fetches the text of the Jabberwock poem from the web and places it into your "c:/jabbertest/" directory, which it creates if needed. It also ensures deletes any files or directories named "jabberwocky2.txt" or "a" in that directory if such exists, since the following test cases make use of their absence.)

? pragma.syntax("0.8")

? def poem := <http://www.erights.org/elang/intro/jabberwocky.txt>
# value: <http://www.erights.org/elang/intro/jabberwocky.txt>
 
? <file:c:/jabbertest>.mkdirs(null); null
? <file:c:/jabbertest/jabberwocky.txt>.setText(poem.getText())
? <file:c:/jabbertest/jabberwocky2.txt>.delete(null); null
? <file:c:/jabbertest/a/b/c>.delete(null); null
? <file:c:/jabbertest/a/b>.delete(null); null
? <file:c:/jabbertest/a>.delete(null); null

"<file:...>" URI Expressions

As explained in URI Expressions, the value of a "<file:...>" URI expression is a java.io.File object. Such a file object doesn't necessarily represent a file that already exists on your file system. It might represent an existing directory, an existing "normal" file (a non-directory), or it might represent a file that doesn't yet exist -- a potential file or directory. You can test which as follows:

? def j1 := <file:c:/jabbertest/jabberwocky.txt>
# value: <file:c:/jabbertest/jabberwocky.txt>

? j1.exists()
# value: true

? j1.isNormal()
# value: true

? j1.isDirectory()
# value: false

Not surprisingly, the jabberwocky.txt file we created earlier exists, and is a normal file, and therefore is not a directory.

? def j2 := <c:/jabbertest/jabberwocky2.txt>
# value: <file:c:/jabbertest/jabberwocky2.txt>
The URI here shows an allowable shorthand. For the convenience of MSWindows users, if the URI begins with a single letter followed by a colon, then a preceding "file:" is assumed. (On MSWindows this single letter is known as the drive letter, and names approximately a mounted volume.)
? j2.exists()
# value: false

? j2.isNormal()
# value: false

? j2.isDirectory()
# value: false

On the other hand, "jabberwocky2.txt" does not yet exist, and is therefore neither normal nor a directory.

? def desk := <file:~/Desktop>
# value: <file:c:/Documents and Settings/millerm1/Desktop/>

? desk.exists()
# value: true

? desk.isNormal()
# value: false

? desk.isDirectory()
# value: true

"desk" now represents our Windows Desktop which, as you can see, is a directory.

? desk.getCanonicalPath()
# value: "c:/Documents and Settings/millerm1/Desktop/"

To aid portability, E enables (and encourages) you to deal with Files using "/" as the path separator character. But you can use "getCanonicalPath" when you want to see what the path-name of the file looks like to your operating system.

? desk.getName()
# value: "Desktop"

? desk.getParent()
# value: "c:/Documents and Settings/millerm1/"

? j1.getName()
# value: "jabberwocky.txt"

? j1.getParent()
# value: "c:/jabbertest/"

"getName()" and "getParent()" both return Strings (not Files) representing the local name of the file within its containing directory, and the name of this containing directory, respectively.

The methods canRead(), canWrite(), delete(), and renameTo(file) all behave just as in Java, and operate on both normal files and directories. See the javadoc-umentation.

Reading Text

As we saw in the Jabberwocky example, if you use a normal File as the collection of a for loop, E assumes the file is a text file, and treats it as a collection of lines.

? var size := 0
# value: 0
 
? for line in <c:/jabbertest/jabberwocky.txt> {
>     size += line.size()
> }
 
? size
# value: 270

To aid portability, each of these lines ends with a newline character, '\n', independent of how newlines are actually represented on the current platform. On Windows newlines are represented with a pair of characters, so the above size is smaller than the Windows file size.

? <c:/jabbertest/jabberwocky.txt>.getText().size()
# value: 270

You can also read the whole (presumed) text file into one big String using "getText()". As you might be able to guess from the agreement on size, getText() also turns platform-specific newlines into newline characters.

When you don't want to read the whole file at once, you instead open a text reading stream on the File using "textReader":

? def reader := <c:/jabbertest/jabberwocky.txt>.textReader()
# value: <a BufferedReader>
 
? reader.readChar()
# value: '\''

? reader.readLine()
# value: "Twas brillig and the slithy toves "

? reader.readString(10)
# value: "Did gyre a"

? reader.readString(1000).size()
# value: 232

"readChar" returns the next character. "readLine" reads a line of text, but, following Java conventions, without the terminating newline. "readString(num)" will read num characters, and return them as a String. However, if there aren't num character left before the end of file, it will return the rest of the characters and leave the stream at end of file.

? reader.readChar() == null
# value: true

When the stream is at end of file, the above read-requests all return null.

? reader.close()
? reader.readChar()
# problem: <IOException: Stream closed>

When you're done with a stream, you should always be sure to close it. That's fine for interactive use, but how can you be sure your program will close the stream when an I/O error might throw you out of your function before you get to your close message? By using try/finally:

? def reader := <c:/jabbertest/jabberwocky.txt>.textReader()
# value: <a BufferedReader>

? var size := 0
# value: 0

? try {
>     while((def line := reader.readLine()) != null) {
>         size += line.size()
>     }
>     size
> } finally {
>     reader.close()
> }
# value: 261

This loop uses a reader to loop through the file one line at a time. The "finally" clause will be sure we close the stream no matter how we exit the try block. If, for example, "reader readLine" throws an IOException (always a possibility), the stream will still be closed.

Notice that the cumulative size is smaller than before. This is because "readLine" produces lines not terminated by any form of newline.

? <c:/jabbertest/jabberwocky2.txt>.exists()
# value: false

? <c:/jabbertest/jabberwocky2.txt>.textReader()
# problem: <FileNotFoundException: c:\jabbertest\jabberwocky2.txt \
#           (The system cannot find the file specified)>

(Note that the above example doesn't yet pass as an Updoc test case due to the line continuation bug.)

Not surprisingly, you can't open a text Reader on a file that doesn't exist. However...

Writing Text

? def writer := <c:/jabbertest/jabberwocky2.txt>.textWriter()
# value: <a TextWriter>
 
? <c:/jabbertest/jabberwocky2.txt>.exists()
# value: true

... you can open a TextWriter, which also causes the file to be created if needed. You then fill the file with content by writing to the TextWriter, and then closing it. For example, to copy the contents of <c:/jabbertest/jabberwocky.txt> into <c:/jabbertest/jabberwock2.txt>:

? try {
>     for line in <c:/jabbertest/jabberwocky.txt> {
>         writer.print(line)
>     }
> } finally {
>     writer.close()
> }

Once again, we use a try/finally expression to ensure that the Writer gets closed no matter how we leave the expression.

"print(object)" prints the stringified form of the object to the Writer. The stringified form of a String is the String itself. The stringified form of an object is what gets printed out after "# value: ". (*** No longer quite true. Need to explain about the quoted form.) For example, the stringified form of a number is just its decimal representation:

? 2 + 3
# value: 5

? 2 / 3
# value: 0.6666666666666666

This is the form that would get written to a file if these values were used as the argument of a "print" message.

? <c:/jabbertest/jabberwocky2.txt>.getText().size()
# value: 270

As we see from the size, our file-copy code copied the whole file.

We can turn the above pattern into a useful little function for copying text files:

? def textFileCopy(srcFile, destFile) :void {
>     def writer := destFile.textWriter()
>     try {
>         for line in srcFile {
>             writer.print(line)
>         }
>     } finally {
>         writer.close()
>     }
> }
# value: <textFileCopy>

Using this function, we can copy files simply:

? textFileCopy(<c:/jabbertest/jabberwocky.txt>,
>              <c:/jabbertest/jabberwocky2.txt>)

Your interactive command-line interpreter already has an TextWriter that's always open, called "stdout", that displays text on your command line interpreter window:

? stdout
# value: <a TextWriter>
  
? stdout.println(2/3)
0.6666666666666666

TextWriters also respond to a "println" message, which is just like "print", except that it also provides a newline on the end. Similarly, there is also a "lnPrint" message that provides a newline on the beginning. If we wanted to write a text-copy function that worked on already open streams, rather than opening and closing them ourselves, we might write:

? def textStreamCopy(reader, writer) :void {
>     while ((def line := reader.readLine()) != null) {
>         writer.println(line)
>     }
> }
# value: <textStreamCopy>

? def reader := <c:/jabbertest/jabberwocky.txt>.textReader()
# value: <a BufferedReader>

? try {
>     textStreamCopy(reader, stdout)
> } finally {
>     reader.close()
> }
Twas Brillig and the Slithy Toves
Did gyre and gimbal in the wabe
All mimsy were the borogroves
and the Momerathes outgrabe.

"Beware the Jabberwock my son
The jaws that bite
the claws that catch
Beware the Jubjub bird and shun
The frumious bandersnatch."

We print to the writer using "println" above, since "reader readLine" returns a line without the terminal newline.

If we replace the line containing "stdout" above with

>     textStreamCopy(reader, stdout indent("..."))

and do the above sequence again, we get

Twas Brillig and the Slithy Toves
...Did gyre and gimbal in the wabe
...All mimsy were the borogroves
...and the Momerathes outgrabe.
...
..."Beware the Jabberwock my son
...The jaws that bite
...the claws that catch
...Beware the Jubjub bird and shun
...The frumious bandersnatch."
...

When you send the "indent(prefix)" message to an TextWriter, it returns a new TextWriter that writes to the same destination, but adding the extra prefix after every newline.

? stdout.indent("...").lnPrint(<c:/jabbertest/jabberwocky.txt>.getText())

...Twas Brillig and the Slithy Toves
...Did gyre and gimbal in the wabe
...All mimsy were the borogroves
...and the Momerathes outgrabe.
...
..."Beware the Jabberwock my son
...The jaws that bite
...the claws that catch
...Beware the Jubjub bird and shun
...The frumious bandersnatch."
...

Finally, there are also global "print" and "println" functions that send the corresponding message to stdout and also return their argument.

? println(2/3)
0.6666666666666666
# value: 0.6666666666666666

Directories

As we saw in the original Jabberwocky example, if you use a for-loop to iterate a File object that is a directory, the loop variable gets bound to each File (normal file or directory) in this directory. With this piece of information, we can now write our first non-trivial useful program:

? def writeDirMap(filedir, writer) :void {
>     writer.lnPrint(filedir.getName())
>     if (filedir.isDirectory()) {
>         writer.print("/")
>         def indentation := (" " * filedir.getName().size()) + "/"
>         def nested := writer.indent(indentation)
>         try {
>             for file in filedir {
>                 writeDirMap(file, nested)
>             }
>         } finally {
>             nested.close()
>         }
>     }
> }
# value: <writeDirMap>

"writeDirMap" is a recursive function only meant to be called from "dirMapper". "filedir" is a File object that may represent either a normal file or a directory. writeMapDir uses "lnPrint" to print the local-name part of filedir onto writer, preceded by a newline. If filedir is a normal file, we're done. If it's a directory, then we suffix the name with a "/", so someone looking at the output can tell it's a directory, and then call ourselves recursively with each File (normal file or directory) in this directory. In this recursive call, we pass a new TextWriter, "nested" which is just like "writer", except that it has an extra prefix string consisting of a number of spaces equal to our local name, followed by a "/". As you'll see, this results in outline-style output in which it's easy to figure out what's under what. This prefix string gets output after the newline written by the call to "lnPrint" in the nested call to "writeDirMap".

? def dirMapper(filedir, destFile) :void {
>     def writer := destFile.textWriter()
>     try {
>         writeDirMap(filedir, writer)
>         writer.println()
>     } finally {
>         writer.close()
>     }
> }
# value: <dirMapper>

Just wraps the top-level call to "writeDirMap" with the opening of a Writer onto destFile, and the insertion of the last newline. We can call "dirMapper" to get a useful textual roadmap to a portion of our directory hierarchy. For example, when I map out my Windows Start Menu:

? dirMapper(<file>["/Windows/Start Menu"],
>           <file:/startMenuMap.txt>)
The beginning of my resulting file looks like
Start Menu/
          /ebash.pif
          /Programs/
          /        /CRT/
          /        /   /CRT Help.lnk
          /        /   /CRT.lnk
          /        /   /README.lnk
          /        /   /ORDER.lnk
          /        /MS-DOS Prompt.pif
          /        /4dos.pif
          /        /Internet Mail.lnk
          /        /Microsoft NetMeeting.lnk

Yours, of course, will differ in the details. If you map your <file:/> instead, go out and get a cup of java. It'll take a while. (My little laptop has over 53,000 files and directories on it, each of which produces a line in the map file.)

We've seen how to create new normal files -- by opening up a textWriter. How do we create directories? With "mkdir" or "mkdirs":

? def abc := <c:/jabbertest/a/b/c>
# value: <file:c:/jabbertest/a/b/c>

? abc.exists()
# value: false

At this point, <file:/jabbertest/a/b/c> is neither a directory nor a normal file.

? abc.mkdirs(null)
# value: true

If <file:/jabbertest/a/b> already existed, and was a directory, then "mkdir" would have been adequate. However, if <file:/jabbertest/a/b> didn't exist, "mkdir" would have failed. "mkdirs", on the other hand, creates the parent directories as necessary in order to create <file:/jabbertest/a/b/c> as a directory.

? abc.exists()
# value: true
? abc.isDirectory()
# value: true
 
? abc
# value: <file:c:/jabbertest/a/b/c/>

abc now prints out with a terminal slash to indicate that it represents a directory.

? def ab := <c:/jabbertest/a/b>
# value: <file:c:/jabbertest/a/b/>

? ab["c"]
# value: <file:c:/jabbertest/a/b/c/>

? ab[".."]
# problem: <SecurityException: ".." not allowed: ..>

Directories are collections, mapping from local file names to the contained Files (normal files or directories). We've already seen that directories support the for-loop the way would expect such a collection to. In addition, we can use the square-bracket indexing operator to get a File of a given name within a directory. However, we can only descend the directory hierarchy. For reasons explained in Capability Programming Patterns E disallows the use of ".." to navigate upwards.

URLs

*** to be written

 

 
Unless stated otherwise, all text on this page which is either unattributed or by Mark S. Miller is hereby placed in the public domain.
ERights Home elang / io 
Back to: URI Expressions No Next Sibling
Download    FAQ    API    Mail Archive    Donate

report bug (including invalid html)

Golden Key Campaign Blue Ribbon Campaign