cs173: Assignment 7

Version 2, 2002-12-04 14:00


For this assignment, you will implement a small scripting language using Scheme's macro system. Like most Unix shells, we will use streams to represent the output of system processes; below, we provide Scheme primitives for streams.

Since this task relies heavily on support libraries, you will need to learn some new features of PLT Scheme as you go. A good resource is the Help Desk, which contains extensive documentation on all of the libraries. We also expect you to email the course staff (cs173@cs.brown.edu) with questions, and we will frequently update the FAQ.

Your language should include the following expressions; we will award extra credit for additional interesting primitives.

Directory listing

    (files re)

This expression produces a stream containing the names of all files in the current directory that match the regular expression re.

File contents

    (lines re filename)

This expression produces a stream containing all lines in the file filename that match the regular expression re.

Command execution

    (run cmd arg1 arg2 ... argn)

This expression produces a stream containing all lines output by the program cmd with arguments arg1, arg2, ..., argn. The subexpressions (cmd, etc.) can be either symbols or strings, and should be implicitly quasiquoted. For example, the following expressions are legal:

  (run /usr/bin/yes)
  (run /bin/ls -l -a)
  (run "/bin/ps" u)
  (run finger ,(string-append "db" "tucker"))

Stream iteration

  (for stream-expr with ([var init-expr] ...) do
     then return-expr)

This expression iterates over the elements in stream-expr, evaluating body-expr each time, and returning return-expr when the stream is empty. The variables var ... are initially bound to init-expr ... and are updated each iteration by the special expression (loop next-expr ...). Also, the identifier it is bound in body-expr to the current stream element.

The for-expression is best illustrated with an example. The following expression prints all logins and the total number at the end:

   (for (run who) with ([n 0]) do
       (printf "~a~n" it)
       (loop (+ n 1)))
     then (printf "total: ~a~n" n))

There are also two shorter forms of for. The following form is useful when only binding one variable:

  (for stream-expr with (var init-expr) do
     then return-expr)

The above example thus could be written as:

   (for (run who) with (n 0) do
       (printf "~a~n" it)
       (loop (+ n 1)))
     then (printf "total: ~a~n" n))

This form binds no variables:

  (for stream-expr do
     then return-expr)

Since the body of the for-expression includes the implicitly bound identifiers loop and it, your macro must produce an expression where these identifier are not automatically renamed; in parlance, you must break hygiene. In lectures, we saw how to write unhygienic macros using define-macro and how to write hygienic macros using syntax-rules. In fact, we can get the best of both worlds — both pattern matching and the ability to break hygiene — by using a variation of syntax-rules named syntax-case. We give examples of syntax-case below.

Streams code

    (define-syntax stream-cons
      (lambda (stx)
	(syntax-case stx ()
	  [(_ f r) (syntax (cons f (delay r)))])))

    (define stream-empty

    (define (stream-empty? s)
      (empty? s))

    (define (stream-first s)
      (first s))

    (define (stream-rest s)
      (force (rest s)))

    (define (stream-display s)
      (unless (stream-empty? s)
	(display (stream-first s))
	(stream-display (stream-rest s))))

syntax-case examples

Here's the time% macro using syntax-case. It is almost identical to the syntax-rules version; the only two differences are the parameter stx and the keyword syntax in the template.

    (define-syntax (time% stx)
      (syntax-case stx ()
	[(_ expr) (syntax
		    (let ([start (current-milliseconds)])
		      (let ([result expr])
			(let ([end (current-milliseconds)])
			    (printf "time: ~a~n" (- end start))

As we mentioned earlier, the benefit of syntax case is that it allows us to break hygiene. The expression (if-it test then else) behaves just like if, except that the variable it is bound to the result of test in both then and else. Here is the macro definition and an example use:

    (define-syntax (if-it stx)
      (syntax-case stx ()
	[(src-if-it test then else)
	 (with-syntax ([it (datum->syntax-object (syntax src-if-it) 'it)])
	   (syntax (let ([it test]) (if it then else))))]))

    (if-it (memq 'b '(a b c)) it 'nope)
    => '(b c)


  1. Are the reg-exp arguments to files and lines strings to be interpreted as reg-exps or are they Scheme reg-exp objects?

    They are strings; for example:

        (lines "^dbt" "/etc/passwd")

  2. Is the filename argument in lines just the name of a file (i.e., string) or a port object? Can we assume that it is in the current directory?

    The argument is the name of a file in the current directory.

  3. What is the desired behavior for a for that does not use loop?

    If the evaluation of a for expression's body does not use loop, the value of the body should be returned (i.e. you should not continue iterating over stream elements, and you should not evaluate the then expression).

  4. Should the stream resulting from run only contain output from stdout, or stderr also?

    Just stdout.

  5. Could you give a small example of how to use Scheme's subprocess?
        (let-values ([(proc p-out p-in p-err) (subprocess #f #f #f "/usr/bin/finger" "dbtucker")])
  6. Do we have to worry about closing ports?


  7. What do we do if we are passed an empty stream in for?

    Just evaluate the then clause.

  8. For the run command, can we assume that we are given a full path to the command, or should we attempt to somehow look things up in $PATH?

    You can assume you are given a full path.

  9. What changed between versions 1 and 2?

    You do not need to search for programs in $PATH (i.e., the answer to the above question).