file lets a Shoddy program treat a text file as what it
usually is: a stack of lines. You hand it a path and get back a plain list of
strings, one per line, or you hand it a list of strings and it lays them
down as lines on disk. That is the whole idea. It sits on top of Shoddy's
whole-file builtins (ReadFile,
WriteFile, AppendFile and their guarded twins).
It does the two fiddly bits you would otherwise do by hand: it splits the text on
newlines (the invisible characters that end a line), and it tidies up the
line endings so a file written on Windows reads back the same as one written
anywhere else. No handles to open or close: you read the file all at once,
or you write it all at once.
A "line" of text ends differently depending on where it grew up, and the reason is mechanical. On a teletype (a typewriter-like printing terminal) returning the carriage to the left margin and rolling the paper up a row were two separate physical acts. So the machines were sent two separate characters (carriage return, then line feed) and early operating systems dutifully stored both. The CP/M → DOS → Windows line kept the pair. Unix, in 1969, decided the file should record the idea of a line ending rather than the choreography of a 1960s printer. It stored a single line feed and translated on the way to the device. Fifty years later both conventions are still with us. That is why "the same file" can be a different length on two machines, and why half the noise in a file comparison (a diff) is invisible characters. This machine takes the Unix view of what a file means: a list of lines, endings nobody's business. It does the tidying at the boundary, so a file written on Windows reads back the same everywhere.
A great deal of everyday data is lines of text: a list of
names, a to-do file, a log, a little config, the output of yesterday's run
that today's run needs to read back. Working with that as one giant string is
awkward: you end up hunting for newlines and worrying about whether the last
line has one. file takes that chore off your hands so you can
think in lines: read a file into a list, loop over it, filter it, sort it,
write it back. Reach for it whenever a program needs to remember something
between runs and that something is naturally a list of lines. When you need
to look individual records up by key rather than read the whole thing top to
bottom, reach for its bigger cousin isam instead.
The whole machine does the obvious things: read all the lines, write all the lines, or add one line to the end. Because reading and writing each touch the whole file in one go, there is nothing to open and nothing to close. You call the word and you are done.
Include "file.shoddy"
Def Main()
WriteLines("names.txt", { "ADA", "LIN", "GRACE" })
AppendLine("names.txt", "HEDY")
Let xs = ReadLines("names.txt")
Print(Length(xs)) ' 4
Print(Nth(xs, 1)) ' ADA
Each(xs, Fn(s) => Print(s)) ' ADA LIN GRACE HEDY, one per line
A few things worth knowing:
WriteLines joins your list with newlines and adds a
final one, so the file ends cleanly. ReadLines knows this and
quietly drops that trailing blank, so what you write is what you read back,
with no phantom empty line at the end.ReadLines treats \r\n and a bare \r as
line endings just like \n, so a file saved with \r\n
endings comes back as the same clean list as one saved with plain
\n. You never have to think about which machine wrote the
file."" gives you { }, not a list
containing one empty string, the sensible answer for "no lines here."WriteLines overwrites whatever was there before with your whole
list. AppendLine leaves the file alone and tacks one more line
onto the end. Handy for logs, where you want to keep adding without rereading
and rewriting the lot each time.Print and Input, reading and writing files are
effectful: they do not only compute a value, they change what is on disk. Keep
them at the edges of your program, where the file-shuffling happens, and let
the rest stay pure.The nine whole-file words the runtime dispatches: not defined here, documented here
These are not file's Defs. The engine dispatches
them (runs them itself) and a Def whose name is a
builtin is refused. They are
listed on this page because file is the machine whose domain they
belong to. A reader who opens the files machine looking for
ReadFile should find it here, rather than be expected to know that
the flat reference exists. They need
no Include: a builtin is callable from any program. The
same nine are documented word for word in
machines/file.shoddy's own header block.
No handles: a text file is read or written whole. A handle is the open-file ticket other languages make you hold and give back; there is none here. Paths resolve against the working directory (the folder the program was started in), and every one of these is effectful.
The Try* words are not duplicates, and the
reason is the shape of error handling in this language. Shoddy has no
catchable errors, so an aborting word cannot be asked "would this work?" The
asking is the doing, and a failure ends the run. The Try*
forms are how a program survives a path it did not choose.
FileExists cannot stand in for any of them: a directory reports
False, an existing file that cannot be opened reports True, and a file that is
there when you ask can be locked by the time a delete lands.
| Word | Description |
|---|---|
| ReadFile(path) | The whole file as one string, newlines and all.
Aborts if the path cannot be read. ReadLines below is the one
that splits it. |
| TryReadFile(path) | The same read with the failure
reported rather than fatal: Ok(text), or
Err(why, 0). The why is
CANNOT READ 'path' (…) ending in one of NO SUCH FILE,
IS A DIRECTORY, ACCESS DENIED or
UNREADABLE, a closed set of Shoddy's own, never the host
platform's exception text, which varies by OS and locale. At is
0: a failure to open a file has no position in it. It
answers a Result rather than a Boolean because, unlike
TryWriteFile, it has a payload to carry on success. |
| TryReadLines(path) | The file as its lines, read natively one
line at a time: Ok(lines), a List Of String, or the
same Err(why, 0) that TryReadFile answers. A line
ends at \n, \r\n or a bare \r, so a
file written on Windows reads back the same as one written anywhere else; a
final newline closes the last line rather than opening an empty one; an empty
file is Ok({ }). ReadLines below is the aborting
form, defined over this one. It is a builtin and not a Def
because a line-oriented read belongs beside TryReadFile: one
pass over the file, the same root guard and the same Err
phrases, rather than a whole-file read split afterwards. |
| Word | Description |
|---|---|
| WriteFile(path, s) | Write the string to path,
replacing whatever was there. Aborts on failure. |
| TryWriteFile(path, s) | The same write, answering True on
success rather than aborting. A Boolean and not a Result: a write
has nothing to hand back. |
| AppendFile(path, s) | Add the string to the end of
path, creating the file if it is not there. Aborts on failure,
and has no guarded twin. |
| Word | Description |
|---|---|
| FileExists(path) | Whether there is a readable file at
path. A directory answers False. Not a pre-flight for the
aborting words; see above. |
| DeleteFile(path) | Delete the file at path. A
missing file aborts. |
| TryDeleteFile(path) | The same delete, answering True if the file went and False if it was not there or could not be removed. |
Every word this machine exports
| Word | Description |
|---|---|
| ReadLines(path) | The lines of the file at path
as a list of strings, one per line: TryReadLines with its failure
made fatal, so a path that cannot be read stops the program with the reason
TryReadLines would have reported. Windows-style files read back
the same as any other, a single trailing newline is dropped, an empty file
gives you an empty list, and a file of any length loads. |
| WriteLines(path, xs) | Writes the list of strings xs
to the file at path, one line each, with a newline after every
line (including the last). Overwrites whatever was there before. |
| AppendLine(path, s) | Adds the single string s as a
new line at the end of the file at path, followed by a newline.
Leaves the existing contents untouched. Good for growing a log. |
| User | How | |
|---|---|---|
| demographics | ReadLines
loads the census rows off disk in the trainer. | |
| iris | ReadLines
loads the flowers off disk in the trainer. | |
| neural | ReadLines
/ WriteLines carry the text model format. | |
| tally | ReadLines reads the golden report the headless suite grades against, line for line. | |
| weather-glass | The offline test reads the captured
fixtures and the golden render with ReadLines. |
None. It stands on the file builtins, and on Join, which
WriteLines lays the lines down with and which is a builtin
too. The split in the other direction is the runtime's, in
TryReadLines.