CSE 331: Batch processing
Course motto: Your job is to deliver code that you know works.
This lecture also returns to Lecture 2's last sections, Stronger and weaker through Knowing a program in pieces. Since these notes do not repeat them, read them in Lecture 2's notes.
Sum, from the command line
Lectures 1 and 2 studied a Sum that asks for numbers, prints them,
and prints their sum. The program below is a different Sum. It
adds up numbers too, but it is 99 lines long, and most of its lines
have nothing to do with adding. Read the whole program before you
read on, with three questions in mind. What does Sum do? What is
good about it? What is not? If some of its Java is new to you, such
as the try on line 59, read past it. These notes come back
to each part that matters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
Compile and run Sum as you did Lecture 1's Sum. With nothing
after java Sum, it reads numbers from the keyboard until you end
the input. Each run in these notes shows the command, then anything
typed, then what Sum printed:
java Sum
3 4 5
12
With -a, Sum prints the average instead:
java Sum -a
3 4 5
4.0
Given the name of a file, Sum reads its numbers from that file.
Here the file nums.txt contains 3 4 5:
java Sum nums.txt
12
So far this Sum looks like Lecture 1's without the prompt and the
Got line. The runs that broke Lecture 1's "user manual" sentence
tell the two apart. On 1 2 three 4, Lecture 1's Sum added 1 and 2
and ignored the rest. On 2147483647 1, it printed a negative
number. Before reading on, predict what this Sum does with each:
java Sum
1 2 three 4
Sum: not an integer: three
java Sum
2147483647 1
2147483648
This Sum stops at the word, and its message says which word it
could not use. It also gets the second sum right. Line 57 declares
sum as a long, Java's 64-bit type for whole numbers, so this
addition does not wrap.
From here on, Sum means this lecture's Sum.
Batch programs
Lecture 1's Sum and this one are both programs of one kind:
Definition: batch program
A batch program reads its input, computes, writes its output, and exits.
What is Sum's input? The numbers are only part of the answer.
The words typed after java Sum are input too. The user
chooses them, and they change what Sum does. These words are
Sum's command-line arguments. Java hands them to Sum's
main as the array args, in the order they were typed. The
loop on line 28 reads them one at a time. An argument that
starts with a dash, such as -a or --precision=2, is an
option. Sum takes any other argument as the name of a
file, and reads the numbers from that file. With no file named,
Sum reads the numbers from standard input, which by
default is the keyboard. Lines 89 and 90 make that choice.
The run on 1 2 three 4 printed a message instead of a sum. The
message is output too, but Sum keeps it apart from its results.
Results go to standard output (lines 78 to 83), and messages
go to standard error (line 96, in fail). When Sum exits,
it also leaves behind an exit status, a number that says
whether the run succeeded. By convention, 0 means success and
anything else means failure. fail exits with status 1
(line 97). Java sets a status even for a program that never
chooses one: 0 if main finishes, and 1 if the program ends in a
stack trace. In a terminal, though, results and messages look the
same, and the exit status does not show at all.
Why keep results and messages apart, if they look the same? Send
standard output to a file with >, and the difference shows. The
file bad.txt contains 3 x. First predict what each of these two
runs shows on the terminal and leaves in total.txt:
java Sum nums.txt > total.txt
java Sum bad.txt > total.txt
Sum: not an integer: x
The first run shows nothing, and leaves 12 in total.txt. The
second still shows its message, since > sends only standard
output to the file. It leaves total.txt empty. Either way, the
file never contains a message where a sum should be.
The exit status says which run failed. On macOS and Linux, echo $?
prints the status of the last command: 1 right after the run on
bad.txt, and 0 right after the run on nums.txt. In Windows
PowerShell, $LASTEXITCODE contains the status instead.
Both macOS and Linux are modeled on an older operating system, Unix.
Unix did not always keep results and messages apart. At first, every
Unix program printed its messages on standard output. That caused
trouble when the output went to a file. It caused worse trouble once
pipes let one program's output flow straight into another program as
input. At Bell Labs, where Unix was built, a program's output could
go to the lab's typesetter, the machine that printed its papers. A
program's messages went there too. Once, several people typesetting
documents got back "a single, beautifully typeset line":
cannot open file foobar.1 Soon after, Dennis Ritchie, one
of Unix's two creators, added standard error. Standard error alone
was not quite enough. With several programs in one pipeline, a
message also had to say which program wrote it. Sum follows that
convention: each of its messages begins with Sum:.
In all, what a user sees of Sum has three parts: its standard
output, its standard error, and its exit status. Sum's
specification has to say what each one is, on every run. A person
at the terminal reads the first two. A script, a program that
runs other programs, can read all three. A script that runs Sum
cannot make sense of a message where it expected a number. Such a
script is like a client in Lecture 2. It relies on what Sum
promises, and it is correct if it works with every version of
Sum that keeps those promises. Unlike a method's client, though,
the script owes Sum nothing. Like any user, it may give
Sum anything at all.
Expecting other programs to read a program's output is an old idea. A
1978 account of the Unix style lists four maxims. The second begins:
"Expect the output of every program to become the input to another,
as yet unknown, program. Don't clutter output with extraneous
information."2 A message among Sum's results would be that
clutter, and Sum keeps it out by writing its messages on standard
error. The words "as yet unknown" matter too. Sum's author cannot
know which programs will read Sum's output, so whatever those
programs rely on has to be in Sum's specification.
Most programs people use are not batch programs. A web browser does not read its input, compute, and exit. It keeps running, and responds to whatever the user does next, until the user tells it to stop. A later lecture takes up programs of that kind.
The user manual, on every run
Sum's specification, then, has to say what its standard output,
its standard error, and its exit status are on every run. Sum
already comes with a user manual: the comment on lines 5 to 19. It
speaks to whoever runs Sum, not to whoever reads its code.
Lecture 1 called a whole program's specification a precise version
of its user manual. Some programmers even write the manual first,
as "a precise specification of the job to be done."3 Let's
see how close this manual comes to being a specification, by
testing it as Lecture 1 tested its first "user manual" sentence:
against runs its author may not have pictured.
The manual promises to read integers, and 2147483648 is an integer:
java Sum
2147483648
Sum: not an integer: 2147483648
Sum reads only the integers that fit in an int, since line 61
asks hasNextInt. Lecture 1's specification needed its range
clause for the same reason. Next, type nothing and end the input.
Sum prints 0, an answer the manual covers if the sum of no
numbers is 0, as Lecture 1 granted. With -a, though, the same
empty input gets a message instead of an average:
java Sum -a
Sum: cannot average zero values
The manual says nothing about this run, nor about the run on
1 2 three 4, nor about the exit status of any run.
Lecture 1 set a standard for every program in this course: every
input a user can produce gets a response the specification
describes, and a stack trace is never that response. So far,
Sum passes the second part: no run of Sum above ends in a
stack trace. Each error in them goes through fail, on
lines 95 to 98, which prints on standard error a message that
starts with the program's name, and then exits with status 1.
That consistency is part of what is good about Sum: every
error above looks the same, to a person and to a script.
Sum fails the first part. The manual describes none of Sum's
error responses, neither the messages nor the exit status. Those are
responses its user sees, so a specification has to describe them.
The manual also sits where its user never sees it, in the source
code. Asking for it with java Sum --help gets only
Sum: unknown option: --help. Exercises 1 and 5 ask you to fix both.
Checks at the edge
Sum does a lot of checking. Lines 28 to 55 check its arguments,
line 61 checks each word of its input, and lines 73 to 75 check that
there is something to average. What are those checks for? Take one
away and see. Below are two changed copies of Sum, each with one
check removed and its lines left blank, so that every other line
keeps its number.
The first copy lacks lines 40 to 43, which check that the precision
is not negative. With the check, Sum answers a negative precision
with a message at once, before it reads any input:
java Sum -a --precision=-1
Sum: --precision must not be negative, got: -1
What does the copy without the check do with the same command,
given the numbers 1 2?
java Sum -a --precision=-1
1 2
Exception in thread "main" java.util.UnknownFormatConversionException: Conversion = '.'
at java.base/java.util.Formatter.parse(Formatter.java:2818)
at java.base/java.util.Formatter.format(Formatter.java:2744)
at java.base/java.io.PrintStream.format(PrintStream.java:1183)
at java.base/java.io.PrintStream.printf(PrintStream.java:1081)
at Sum.main(Sum.java:78)
The negative precision reached the printf on line 78, which builds
its format from the precision. A precision of 2 gives the format
%.2f, which prints two digits after the decimal point. A
precision of -1 gives %.-1f, which is not a format printf
accepts. So the call throws, and the user gets a stack trace.
With the check on lines 40 to 43 in place, a negative
precision never reaches that call.
The second copy lacks lines 73 to 75, which check that some number
was read. On empty input with -a, it does not crash:
java Sum -a
NaN
NaN, short for "not a number," is what the division on line 76
gives when it divides 0 by 0. Worse, the exit status is 0, so the
run reports success. A script that runs java Sum -a and checks the
status would take NaN for an average. A crash would at least have
said that something went wrong.
What should the average of no numbers be? Sum refuses to give
one, and prints a message. Printing 0 would be just as reasonable.
There is no single right answer, but "the one guaranteed wrong
answer is to ignore the problem."4 The copy without
lines 73 to 75 does just that. A program that did the same once
stopped a Navy cruiser, the USS Yorktown, at sea for a couple of
hours, after a crew member entered a zero and the program divided
by it. What matters is that the manual says which answer Sum
gives, and so far it says neither.
So each check stands at what Lecture 1 called the edge of the
program, between the user and the computation. The user owes Sum
nothing, so the checks take whatever arrives and pass on only what
the computation can handle: a precision that is not negative, and
a count that is not zero.5 That suggests a way to
organize a program like Sum. Keep the code that deals with the
user (the arguments, the messages, the exit status) apart from the
code that computes the answer. Sum does nearly all its work in
main, both kinds together. For a program of 99 lines, keeping
the work there and splitting it up can both be defended.
Split it, and Lecture 2 says what the pieces should be: methods with
specifications. The checks stay at the edge, in main. A method
inside may then have a precondition, such as a count that is not
zero, since its client is main, code that can be held to a
promise. Lecture 2's exercise 6 made the same argument. Exercise 3
below asks you to do the split.
Homework 1's Task 4 has the same shape. Gradebook is a batch
program too, with its user manual at the top of its source. Its
main has to check the three files it reads before it calls
computeCourseGrades, whose @requires clauses assume those checks
passed. Task 4 also asks for Lecture 1's standard: when the user
makes a mistake, a message and an exit, never a stack trace.
Exercises
Each answer is folded under its question. Write yours first.
1. Repair Sum's user manual until it is true of every run of
Sum in these notes, error messages and exit status included.
Answer
-
One repair. It keeps the manual on lines 5 to 19 word for word, and adds three paragraphs:
Reads whitespace-separated integers and prints either their sum (default) or their average (with -a/--average). Usage: java Sum [options] [file] Options: -a, --average print the average instead of the sum --precision=N digits after the decimal point (only valid with -a; defaults to full precision) If a file argument is given, input is read from that file; otherwise it is read from standard input. Each integer must be from -2147483648 to 2147483647. Sum prints its result on standard output and exits with status 0, unless it finds one of the mistakes below. The sum of no integers is 0. On a mistake, Sum prints one line on standard error, starting "Sum: ", and exits with status 1. It stops at the first mistake it finds: - an unknown option, or more than one file - an N in --precision=N that is not an integer from 0 to 2147483647 - --precision without -a - a file that cannot be read - a word of input that is not an integer in the range above - -a with no integers to average One exception: if Sum finds no mistake, a precision too large for Java to print, such as 2147483647, ends in a Java stack trace, with exit status 1. -
Where the additions come from. Most describe a run in these notes. Two mistakes appear in no run here: a precision that is not an integer, and
--precisionwithout-a. They come from the checks on lines 28 to 55. -
The exception. A manual true of every run has to say what
java Sum -a --precision=2147483647does with integers to average: it ends in a stack trace. Like Lecture 1's range clauses, that sentence is unpleasant. Its unpleasantness is information, since it points at a change to the program itself: a largest precision, stated in the manual and checked inmain, as in exercise 3's answer. -
Homework 1. Task 3 asks for a user manual for
Histogram.
2. Find an input whose error message misleads the user.
Answer
-
An option without its dashes.
Sumtakesprecision=4for the name of a file:java Sum precision=4 Sum: could not read input: precision=4 (No such file or directory)The message is about reading a file, but the mistake is in an option. Name a file after it, and the message blames that file, the one argument that was right:
java Sum precision=4 nums.txt Sum: too many file arguments: nums.txt -
An integer too large.
2147483648is an integer, butSumsays that it is not. The run on it in The user manual, on every run shows the message for a word of input. A precision gets a message that makes the same claim:java Sum -a --precision=2147483648 Sum: --precision must be an integer, got: 2147483648 -
What they share. Each message describes the input as
Sum's code saw it: a file that would not open, a second file, a word or a precision that would not parse as anint. None says what the user most likely meant. A message that said whatSumexpected, such as an integer from -2147483648 to 2147483647, would tell the user what to fix.
3. Split Sum as Checks at the edge suggests. Leave in
main the code that deals with the user: the arguments, the input,
the messages, and the exit status. Move into a method the code that
computes what Sum prints from the numbers read, and write that
method's specification. Which of Sum's checks does the method's
precondition rely on, and why may it rely on them?
Answer
-
One split.
mainkeeps every check, the one on lines 73 to 75 included. It then prints what a new method returns:/** * Returns the result Sum prints for the numbers it read. * * @param sum the sum of the numbers read * @param count how many numbers were read * @param average whether to give their average instead of * their sum * @param precisionGiven whether to write the average with a * set number of digits after the decimal point * @param precision that number of digits * @requires count > 0, if average is true * @requires 0 <= precision <= 100, if average and * precisionGiven are true * @return sum, if average is false; otherwise sum divided by * count, written with precision digits after the decimal * point if precisionGiven is true, and as println writes a * double if not */ static String answer(long sum, long count, boolean average, boolean precisionGiven, int precision) -
What the precondition relies on. Two of
Sum's checks: lines 73 to 75, for a count that is not zero, and lines 40 to 43, for a precision that is not negative. The two changed copies above show what each check prevents. -
A check
Sumlacks. Writing the precondition means saying which precisionsanswercan handle. A precision that is not negative is not enough. As the footnote in Checks at the edge says, a precision of 2147483647 asks for more digits than a Java string can hold. So the precondition has to name a largest precision.mainthen needs one more check: a precision above the largest gets a message and exit status 1, as a negative one does. The largest precision here, 100, is a choice. Past about 17 significant digits, Java prints only zeros anyway. What matters is that the manual states it. -
Why a precondition here. The client of
answerismain, code that can be held to a promise, as Lecture 2's exercise 6 argued. The user ofSumowes it nothing, soSumcan require nothing of the user. The same conditions are therefore checks at the edge and preconditions inside. -
The other checks.
Sum's other checks guardmain's own work, reading the arguments and the input, or catch a mistake by the user, such as--precisionwithout-a.answergets only whatmainhas already read, so its precondition needs none of them. -
Helpers at the edge.
mainmay hand some of its checks to helpers, such as a method that reads the arguments. A helper like that deals with the user too, so it stands at the edge withmain: it checks the user's words, and requires nothing of them.
4. Here are two manuals for what Sum does on a word that is not
a whole number:
- M1. It prints
Sum: not an integer:and the word on standard error, and exits with status 1. - M2. It exits with a nonzero status.
Which is stronger? A script runs Sum and checks only whether Sum
succeeded. Against which of M1 and M2 is the script correct?
Answer
-
M1 is stronger. The two have the same precondition, none, since the user owes
Sumnothing. So, as in Lecture 2, we compare only what they promise. Exit status 1 is in particular a nonzero status, so every version ofSumthat satisfies M1 also satisfies M2. The reverse fails: a version that exits with status 2 and prints nothing satisfies M2 and not M1. -
The script is correct against both. On such a word, it relies only on a nonzero status, which M2 promises. So it works with every version of
Sumthat satisfies M2. Every version that satisfies M1 satisfies M2 too, so the script works with those as well. -
A script that reads the message is correct against M1 alone. Suppose it reports the bad word by taking what follows
Sum: not an integer:. A version that satisfies M2 by exiting with status 2 and printing nothing leaves it no word to report. -
Which manual to publish. As Lecture 2's Which is better? says, every promise binds the implementer. A manual that promised M1's message word for word would rule out the clearer message that exercise 2's answer suggests, since that change would break the script that reads the message. The manual in exercise 1's answer promises less about the message: a line on standard error starting
Sum:, but not the words after it.
5. Sum's user manual is a comment in its source, where someone
running Sum never sees it. Asking for help gets only an error:
java Sum --help
Sum: unknown option: --help
Give Sum a --help option that prints the manual. When the
arguments contain a mistake, such as an unknown option, make Sum
say how to get the manual. Then add to the manual what each case
prints, where, and with what exit status.
Answer
-
The manual in one place. A string constant,
USAGE, holds the manual. It is written as a text block, a string between triple quotes that keeps its line breaks.--helpprints it on standard output and exits with status 0, since the user asked for the manual and got it:} else if (arg.equals("--help")) { System.out.print(USAGE); System.exit(0); } else if (arg.startsWith("--precision=")) {The
/**comment can then describe the class for programmers instead of repeating the manual. -
A hint on each mistake in the arguments. It is "better to tell people concisely how to do things right than tell them only that they did something wrong."6 The checks on lines 28 to 55, which find mistakes in how
Sumwas run, call a new method instead offail:private static void usageError(String message) { System.err.println("Sum: " + message); System.err.println( "Try 'java Sum --help' for more information."); System.exit(2); }java Sum --bogus Sum: unknown option: --bogus Try 'java Sum --help' for more information.The hint follows GNU's
ls, which on an unknown option printsTry 'ls --help' for more information. -
Status 2 for those mistakes. With 1 kept for bad input, a script can tell whether it ran
Sumwrong or gave it dataSumcould not use. GNU'slsexits with status 2 on an unknown option. Python'sargparseprints a short usage line instead of a hint, and also exits with status 2. In Java, the picocli library documents 2 as its status for a usage error. The convention is common, not universal: thelson macOS exits with status 1. -
The manual's new lines.
USAGEholds exercise 1's manual with three changes. Under Options, a line says what--helpdoes. A new sentence says that with--help,Sumprints the manual on standard output and exits with status 0. And the paragraph on mistakes splits them by exit status:On a mistake, Sum prints a line on standard error, starting "Sum: ". It stops at the first mistake it finds. A mistake in the arguments adds a second line, "Try 'java Sum --help' for more information.", and exits with status 2: - an unknown option, or more than one file - an N in --precision=N that is not an integer from 0 to 2147483647 - --precision without -a A mistake in the input exits with status 1: - a file that cannot be read - a word of input that is not an integer in the range above - -a with no integers to average
Further reading
- The maxims. M. D. McIlroy, E. N. Pinson, and B. A. Tague, "UNIX Time-Sharing System: Foreword" (The Bell System Technical Journal, 1978). In six pages, the foreword describes a style in which large tasks are built from small, loosely coupled parts, then gives the four maxims under the heading "Style," with an illustration of each.
- How the conventions arose. M. D. McIlroy, "A Research UNIX Reader" (Bell Labs Computing Science Technical Report 139, 1987). McIlroy, who was there, tells in short notes how the early Unix programs and their manual took shape. The note on pipes tells how messages moved off standard output and came to name their program. The copy online has the notes but not the manual pages they annotate.
- The Unix philosophy. Eric S. Raymond, The Art of Unix
Programming (2003). Chapter 1 quotes the four maxims,
then sets out seventeen rules of the Unix tradition. One is the
Rule of Repair: "when you must fail, fail noisily and as soon as
possible," since the worst bugs are those whose damage shows up
only much later. The copy of
Sumwithout its count check breaks this rule: it printsNaNand reports success. - Command-line programs today. Aanand Prasad, Ben Firshman,
Carl Tashian, and Eva Parish, Command Line Interface
Guidelines, a guide that updates the Unix tradition and
puts the person at the terminal first. Three of its basic rules
are this lecture's: exit status 0 on success and nonzero on
failure, results on standard output, and messages on standard
error. Later sections advise printing help on
--help, as in exercise 5, and checking input early with understandable errors, as in Checks at the edge.
-
Stephen Johnson, who worked on Unix at Bell Labs, tells of the typeset line in a 2013 post to the Unix Heritage Society's mailing list. The rest of this history comes from Doug McIlroy, another of Unix's builders, in "A Research UNIX Reader" (1987). ↩
-
M. D. McIlroy, E. N. Pinson, and B. A. Tague wrote the account, as the foreword to a 1978 issue of The Bell System Technical Journal on the Unix system. The maxims are often credited to McIlroy alone. ↩
-
Brian Kernighan and P. J. Plauger recommend writing the manual first, in Software Tools in Pascal (1981), section 1.5. ↩
-
Brian Kernighan and Rob Pike ask the same question of a function that averages an array, in The Practice of Programming (1999), section 6.1. The Yorktown is their example of the cost. ↩
-
Nearly. Given any numbers,
java Sum -a --precision=2147483647ends in anOutOfMemoryErrorand a stack trace at line 78: the format asks for more digits than a Java string can hold. Specifications leave out runs on a computer without the memory to finish, as Lecture 1 noted. This run fails on every computer, though. So the precision has a limit, like the range of anint. The manual has to state it, as a largest precision, andmainthen has to check it. ↩ -
Brian Kernighan and P. J. Plauger give this reason for the usage message of their
translitprogram, in Software Tools in Pascal (1981), section 2.6. ↩