On this page:
Code
7.4.0.4

10 Lab Languages via Macros

Matthew Flatt

Start with "pfsh1.rkt".

Exercise 23. Add define to pfsh to bind an name to the string output of a run command. Save you new language as "pfsh2.rkt". For example,

"use-pfsh2.rkt"

#lang s-exp "pfsh2.rkt"
(define me (run whoami))
me

should define me and then print it out as a Racket string. Just writing me will cause the string to print because we are (so far) using #%module-begin from racket, and that #%module-begin prints the result of any top-level expression.

The with-output-to-string function from racket/port is probably all of the run-time support that is needed for define.

Exercise 24. Add input redirection to run by allowing < followed by a defined identifier at the end of a run form. You’ll probably find syntax/parse’s ~datum or #:datum-literals handy, as well as with-input-from-string from racket/port. Call the extended language "pfsh3.rkt".

"use-pfsh3.rkt"

#lang s-exp "pfsh3.rkt"
(define l (run ls))
(run wc -l < l)

Exercise 25. Symbols are not always convenient to represent arguments to programs. For example, trying to running ls -1 as

(run ls -1)

doesn’t work, because -1 is a number.

Adjust the pfsh implementation to allow strings as well as symbols for program names as arguments. Save you new language as "pfsh4.rkt". For example,

"use-pfsh4.rkt"

#lang s-exp "pfsh4.rkt"
(run ls "-1")

should produce single-column output.

For now, the run form should allow only identifiers and immediate strings as subforms. It should complain with a nice error message if anything else appears, such as a number or a parenthesized form.

You may find that your first attempt doesn’t work and produces an error like “literal data is not allowed” for strings. We’ll explain that error this afternoon. Meanwhile, avoid that kind of error by using an explicit quote form around any literal string that your macro puts in an expansion.

The best implementation for this exercise will involve a new syntax class, perhaps named run-arg.

Exercise 26. Our run macro implementation suppresses a #t or #f output by wrapping a call to the run function with void. That choice is inconvenient for adding a && operator that chains from one command to another only if the first command succeeds:

(&& (run test -f demo.txt)
    (run cat demo.txt))

A better solution to define the run form so that it returns the success boolean, but also change #%module-begin so that it doesn’t print the results of forms in the module body.

Since adjusting the way results are treated in a module body is a common problem, Racket includes a make-wrapping-module-begin function from syntax/wrap-modbeg.

Note that syntax/wrap-modbeg provides make-wrapping-module-begin for-syntax, which means that (require syntax/wrap-modbeg) makes make-wrapping-module-begin available for compile-time expressions instead of run-time expressions. You don’t need to use for-syntax as in (require (for-syntax syntax/wrap-modbeg)).

Change #%module-begin and run in pfsh, and add && so that the following example shows the content of "demo.txt" if it exists in the current directory, and it should print nothing if "demo.txt" does not exist.

"use-pfsh5.rkt"

#lang s-exp "pfsh5.rkt"
(&& (run test -f demo.txt)
    (run cat demo.txt))

Exercise 27. Experienced /bin/sh programmers include set -e in their scripts to make sure the script stops when a command errors. The notion of “errors” is subtle, however. If two commands are combined with && and the first one has a failing status code, the combination doesn’t count as an error. If the second one is run and has a failing status code, the combination counts as an error. At the same time, the combination counts as a failure for the purpose of nested &&s if either the first or second expression fails.

Implement those rules for pfsh (without requiring the programmer to write set -e).

"use1-pfsh6.rkt"

#lang s-exp "pfsh6.rkt"
(run cat demo.txt)
(run echo cat worked) ; don't get here if "demo.txt" doesn't exist

"use2-pfsh6.rkt"

#lang s-exp "pfsh6.rkt"
(&& (run test -f demo.txt)
    (run cat demo.txt))
(run echo cat maybe worked) ; always get here

Code

Code and solutions