I/O Operations in SNOBOL
Learn console input, file reading and writing through variable association, and formatted output in SNOBOL with Docker-ready examples
Most languages do I/O with function calls: print, read, open, close. SNOBOL does it with variables. You met this in Hello World — assigning to OUTPUT prints a line — but that was only the surface of a genuinely unusual design. In SNOBOL, a variable can be associated with a data stream. Assign to an output-associated variable and a record is written; merely reference an input-associated variable and a fresh record is read. I/O isn’t a library you call; it’s a side effect of assignment and evaluation, woven into the same statement machinery as everything else.
The second half of the design is pure SNOBOL: end of file is failure. When an input-associated variable has no more data to give, referencing it doesn’t return a sentinel or raise an exception — the statement simply fails, and the :F(LABEL) goto field catches it. The read loop, the EOF test, and the branch are all one line. If you worked through the control flow tutorial, you already know this machinery; here it becomes the engine of every file-processing program.
In this tutorial you’ll read lines from standard input until end of file, create your own file associations with the INPUT() and OUTPUT() functions to write a file and read it back, and build formatted, columnar output using nothing but SNOBOL’s string operations — because a language whose entire worldview is string manipulation needs no printf.
Reading Standard Input
The predefined variable INPUT is associated with standard input: each time a statement references it, one line is read. At end of file the reference fails, so the idiomatic read loop is a label, a read, and a :F goto.
Two idioms in this example deserve a note. First, TRIM(INPUT) — classic SNOBOL4 came from the punch-card era, where every input record was padded with blanks to a fixed width, so programmers reflexively wrapped INPUT in TRIM() to strip trailing blanks. Modern CSNOBOL4 reads variable-length lines, but the idiom remains standard practice and is harmless. Second, TOTAL + LINE adds a string to a number directly: SNOBOL’s weak typing converts numeric strings to numbers on the spot, so input never needs an explicit parsing step.
Create a file named io_stdin.sno:
| |
The whole EOF protocol lives in :F(DONE): when the pipe runs dry, INPUT fails, the failure propagates through TRIM() to the statement, and control jumps out of the loop. There is no separate end-of-file check because failure is the end-of-file check. The TERMINAL line goes to standard error (as introduced in Hello World), so piping this program’s standard output elsewhere won’t capture the status message.
File I/O Through Variable Association
Here is where the variable-association model pays off. The built-in functions INPUT() and OUTPUT() create new associations: OUTPUT(.LOG,20,,"journal.txt") binds the variable LOG to the file journal.txt on unit 20, opened for writing. From then on, every assignment to LOG writes one line to the file — the same syntax as printing to the console, just a different variable. The .LOG argument is the name operator: it passes the variable itself rather than its value, so the function can attach the association to it.
Unit numbers are another piece of mainframe heritage: each open file gets a small integer channel (avoid 5 and 6, traditionally reserved for standard input and output). ENDFILE(20) closes unit 20, flushing its contents — which is exactly why it appears between the writing and the reading below. Reading back uses the mirror-image INPUT() association: after it, every reference to LINE reads the next line of the file, and failure signals EOF just as it did for standard input.
Create a file named io_files.sno:
| |
Notice what’s absent: no file handles passed around, no read or write calls. Once the association exists, file I/O looks identical to every other assignment in the program. Opening a file for output creates it (or overwrites an existing one), and I/O trouble surfaces the same way everything in SNOBOL does — as statement failure that a :F goto can catch, demonstrated here by the EOF exit from the read loop.
Formatted Output with String Operations
SNOBOL has no format strings. It doesn’t need them: the language is a string-formatting engine, so reports are built by computing the exact string you want and assigning it to OUTPUT. Two vanilla built-ins do most of the work. DUPL(S,N) returns string S repeated N times, and SIZE(S) returns a string’s length — so DUPL(" ", W - SIZE(S)) S right-justifies S in a field of width W. Wrapped in the user-defined functions you met in the functions tutorial, these become reusable column formatters.
Create a file named io_format.sno:
| |
Weak typing quietly helps again: RJ(75,6) passes an integer where the function body applies SIZE() and concatenation, and SNOBOL converts the number to its string form automatically. The padding expression, the column widths, even the ruled lines are ordinary string values — formatting is just another string program.
Running with Docker
| |
The stdin example needs Docker’s -i flag so the piped input reaches the program; without it, the first reference to INPUT immediately fails.
Expected Output
Running io_stdin.sno with the piped input 12, 30, 58 (the first line arrives via standard error):
Reading from standard input...
Line 1: 12
Line 2: 30
Line 3: 58
Read 3 lines
Total = 100
Running io_files.sno:
1: day 1: learned OUTPUT
2: day 2: learned INPUT
3: day 3: learned file association
journal.txt held 3 lines
Running io_format.sno:
ITEM COST
----------------
cards 75
tape 1250
printer 9
----------------
TOTAL 1334
Key Concepts
- I/O is variable association — assigning to an output-associated variable writes a record, and merely referencing an input-associated variable reads one; there are no read or write calls
- End of file is failure — an exhausted input variable makes the statement fail, so
:F(LABEL)is simultaneously the EOF test and the loop exit, the same mechanism that drives all SNOBOL control flow INPUT()andOUTPUT()create new associations —OUTPUT(.LOG,20,,"file")binds a variable to a file for writing andINPUT(.LINE,21,,"file")for reading; the.VARname operator passes the variable itselfENDFILE(unit)closes a unit and flushes it — close an output unit before reading the same file back, and avoid units 5 and 6, traditionally reserved for standard input and outputTRIM(INPUT)is the canonical read idiom — a habit from padded punch-card records that remains standard, harmless practice on modern variable-length input- Weak typing makes input parsing free — numeric strings read from a stream work directly in arithmetic, and numbers convert to strings automatically when concatenated or printed
- Formatting is string computation — there is no
printf;DUPL()andSIZE()build padding and rules, and small user-defined functions likeLJ/RJgive reusable column layout TERMINALkeeps diagnostics separate — status messages assigned toTERMINALgo to standard error, so they never contaminate piped or redirected standard output
Running Today
All examples can be run using Docker:
docker pull esolang/snobol:latest
Comments
Loading comments...
Leave a Comment