Rash:   The Reckless Racket Shell
1 Stability
2 Rash Guide
3 Rash Reference
rash
make-rash-transformer
define-line-macro
default-line-macro
cd
run-pipeline
4 Interactive Use
result-n
return-n
set-default-pipeline-starter!
current-prompt-function
5 Demo stuff reference
=map=
=filter=
in-dir
val
6 Code and License
6.12

Rash: The Reckless Racket Shell

William Hatch <[email protected]>

 (require rash) package: rash

Rash, adj 1. Hurrying into action or assertion without due caution and regardless of prudence, hasty, reckless, precipitate. “A rash programmer is likely to produce shell scripts.

1 Stability

Rash is not stable.

But it’s getting closer, and it’s definitely usable as an interactive shell/repl.

2 Rash Guide

Rash is a shell language embedded in Racket. It has a concrete syntax that is amenable to quick and easy interactions without lots of punctuation overhead. It aims to allow shell-style interaction and programming to be freely mixed with more general-purpose Racket code. Like shells you can use it as a day-to-day interface for computing, run programs, wire processes together, etc. You can copy interactive code into a file and have a working script. Then you can generalize from there. However, you have access to all of Racket, its data structures, its libraries, its macro system, and its other DSLs as you make your scripts more general. Also it allows shell-style process wrangling to be mixed with Racket code, pipelines of functions that pass objects, and much more. You can gradually move between shell-style Rash code and more normal and general Racket code in different parts of a script, or throw verbatim interactions directly into existing programs as you explore the solution space.

Here follows a quick overview that assumes some knowledge of shell languages like Bash as well as Racket.

Rash can do the sorts of things you expect from shell languages, and a lot of cues for syntax have been taken from traditional Bourne-derived shells. The following program works as you would expect.

Note that Rash is not remotely Posix Shell compliant.

#lang rash
cd project-directory
echo How many Racket files do we have?
ls *.rkt | wc -l

You can use Racket functions in pipelines.

;; This returns the hostname as a string in all caps
cat /etc/hostname |> port->string |> string-upcase

Pipelines pass objects. When a process pipeline segment is next to a function pipeline segment, it passes a port to the function segment. When a function segment is followed by a process segment, the return value of the function segment is printed to the stdin of the process.

The |>> operator is like the |> operator except that if it receives a port it converts it to a string automatically. So the above example can be simplified as:

cat /etc/hostname |>> string-upcase

You can also make pipelines composed entirely of Racket functions.

|> directory-list |> map delete-file

Pipelines always start with an operator, and if none is specified the default-pipeline-starter is inserted. Pipeline operators are user-definable with define-pipeline-operator. Defining new operators can help make common patterns shorter, simpler, and flatter. For instance the =map= operator wraps the map function, allowing you to specify just the body of a lambda.

;; These two are the same.
|> list 1 2 3 |> map (λ (x) + 2 x)
|> list 1 2 3 =map= + 2

Pipeline operators are macros, and therefore can play any of the tricks that macros generally can in Racket. The | operator can auto-quote symbols, turn asterisks into glob expansion code, etc. The |> operator can detect whether the current-pipeline-argument is used and insert it automatically.

If you put parentheses in the middle of a pipeline, you escape to normal Racket code.

;; This will either show hidden files or give a long listing
ls (if (even? (random 2)) '-l '-a)

Lines of code in Rash are command pipelines by default, but there are key words called line-macros that can change the behavior arbitrarily.

Line-macros can be used to make C-like control-flow forms like for, try/catch, etc, to make one-off non-pipeline forms like cd, or even to make entirely new and different line-oriented languages.

in-dir $HOME/project {
  make clean
}

For instance, in-dir executes code with current-directory parameterized based on its first argument. Note that logical rash lines don’t necessarily line-up with physical lines. Newlines can be escaped with a backslash, commented out with multiline comments, and if they are inside parentheses or braces they are handled by the recursive read.

echo This is \
     all #|
     |# one (string-append
             "logical"
             "line")

Braces trigger a recursive line-mode read. They are available in line-mode as well as in the embedded s-expression mode. So they can be used to create blocks in line-mode as with the above in-dir example, or be used to escape from the s-expression world to line-mode.

Note that subprocess pipelines are connected to stdout and stdin in the REPL and at the top level of #lang rash. The most common thing you want when embedding some Rash code is for subprocess output to be converted to a string. Using #{} switches to line-mode with defaults changed so that subprocess output is converted to a string and passed through string-trim.

;; I do this a lot when I don't remember what a script does
cat #{which my-script.rkt}

TODO - how to more generally parameterize such settings.

Every line in Rash is actually a line-macro. If a line does not start with a line-macro name explicitly, default-line-macro is inserted. By default this is run-pipeline in Rash.

TODO - actually the default is run-pipeline/logic, which I haven’t documented yet, which adds && and ||.

You can also write normal parenthesized Racket code. If the first (non-whitespace) character on a line is an open parenthesis, the line is read as normal Racket code and no line-macro is inserted.

(define flag '-l)
ls $flag

Note that practically all Racket code starts with an open-paren, so Rash is almost a superset of normal Racket. The only thing lost is top-level non-parenthesized code, which is really only useful to see the value of a variable. Most programs in #lang racket/base could be switched to #lang rash and still function identically, and I never launch the Racket repl anymore because rash-repl is both a shell and a full Racket repl.

(define x 1234)
;; Now let's see the value of x.
;; We can't just write `x`, but we can do any of these:
(values x)
|> values x
echo $x
val x

Avoiding line-macros by starting with a paren causes an annoying inconsistency – you can’t have a line-macro auto-inserted if the first argument to the line macro is a parenthesized form.

;; We want to choose a compiler at runtime.
run-pipeline (if use-clang? 'clang 'gcc) -o prog prog.c
;; If we leave off the line-macro name, it will not be inserted
;; because the line starts with a parenthesis
(if use-clang? 'clang 'gcc) -o prog prog.c

This problem can be fixed by prepending the line-macro name or by using brackets instead of parentheses. The issue doesn’t come up much in practice, and it’s a small sacrifice for the convenience of having both normal Racket s-expressions and Rash lines.

What else belongs in a quick overview? Pipelines with failure codes don’t fail silently – they raise exceptions. More fine-grained behavior can be configured. There are probably more things I should say. But you can read the references for Rash, run-pipeline, and Linea. (Rash is really just a combination of Linea and the Shell Pipeline library.) I will replace this with better documentation soon, but I wrote this up quickly to replace the even worse and terribly out-of-date documentation that was here before.

Linea documentation: Linea: line oriented reader

Pipeline macro documentation: Pipeline Macro Library

3 Rash Reference

Note that all the pipeline stuff (run-pipeline, =unix-pipe=, =object-pipe=, define-pipeline-operator, etc) are documented in the Pipeline Macro Library module.

TODO - rash configuration forms, reader modification...

syntax

(rash options ... codestring)

Deprecated.

Read codestring as rash code.

Options:

#:out sets the default output port or transformer for unix pipelines (as run by run-pipeline). The default runs port->string on the output.

#:in sets the default input port for unix pipelines (as run by run-pipeline). The default is an empty port.

#:err sets the default error port for unix pipelines (as run by run-pipeline). The default is to turn the errors into a string that is put into the exception generated by an error in the pipeline.

#:default-starter sets the default starting pipeline operator for run-pipeline when one is not explicitly given in the pipeline. The default is one of the simple unixy ones... TODO - this default will probably change.

#:default-line-macro sets the default line-macro for lines that don’t explicitly have one. The default is the run-pipeline line-macro.

TODO - options for changing the reader, etc.

Note that the input/output/error-output have different defaults for the rash macro than for the #lang or repl.

syntax

(make-rash-transformer options ...)

Deprecated.

This takes all the same options as rash, but doesn’t take a code string. It produces a transformer like rash, but with different default values for the available options.

(define-syntax my-rash (make-rash-transformer #:default-starter #'=basic-object-pipe=))

Note that the default #lang rash has its input/output/error-output as stdin/stdout/stderr, which is different than the rash macro.

syntax

(define-line-macro name transformer-expr)

Defines a line-macro (the type of macro that overrides the behavior of a rash line).

#lang rash
(require (for-syntax racket/base syntax/parse))
(define-line-macro my-for
  (syntax-parser
    [(_ i:id (~datum in) from:id ... (~datum do) body:expr)
     #'(for ([i (list 'from ...)])
          body)]))
 
my-for f in file1.txt file2.txt do {
  rm $f
}

Syntax parameter used to determine which line macro to place when one is not explicitly given.

TODO - example setting and what is the default.

line-macro

(cd directory)

Change directory to given directory. The directory is quoted, so just put a literal path or a string.

If no argument is given, it changes to the user’s home directory.

Eventually this will be replaced by a better version, but it should be backwards compatible.

line-macro

(run-pipeline arg ...)

Same as shell/pipeline-macro/run-pipeline, except wrapped as a line-macro.

4 Interactive Use

You can run the repl by running racket -l rash/repl. An executable named rash-repl is installed in Racket’s bin directory, so if you have it on your path you can run rash-repl instead. It has no line editing currently, so it’s a little nicer if you run it with the rlwrap command. (readline support, including basic tab completion, is written, but depends on features in the development version of Racket’s readline FFI library.)

Various details of the repl will change over time.

Note that in the repl the default input/output/error-output are to the stdin/out/err connected to the terminal unless you change them. This is different than the rash macro, and allows you to do things like run curses programs that have access to terminal ports.

The repl can be customized with rc files. First, if $HOME/.config/rash/rashrc.rkt exists, it is required at the top level of the repl. Then, if $HOME/.config/rash/rashrc (note the lack of .rkt) exists, it is evaluated at the top level more or less as if typed in (much like rc files for bash and friends).

A few nice things (like stderr highlighting) are in a demo-rc file you can require. To do so, add this to $HOME/.config/rash/rashrc:

(require rash/demo/demo-rc)

(Rash actually follows the XDG basedir standard – you can have rashrc.rkt or rashrc files in any directory of $XDG_CONFIG_HOME or $XDG_CONFIG_DIRS, and the rash repl will load all of them)

The repl uses the readline module for line-editing and completion. The readline module by default uses libedit (or something like that) instead of the actual libreadline for licensing reasons. Libedit doesn’t seem to handle unicode properly. Installing the readline-gpl package fixes that (raco pkg install readline-gpl).

All the following repl functions are not stable.

procedure

(result-n n)  any/c

  n : integer?
Only available in the repl. Return the result of the nth interactive command.

procedure

(return-n n)  any/c

  n : integer?
Only available in the repl. Like result-n, but if the result is a pipeline, get the return value of it. In the repl, the default line-macro is like run-pipeline, but always prepending the &pipeline-ret flag. So you get pipeline objects instead of their results generally, and the prompt handles it specially to see the return value, but also to potentially use other information from the pipeline object.

syntax

(set-default-pipeline-starter! new-starter)

Only available in the repl. A line-macro that mutates the default pipeline starter used in the repl. It’s not really hygienic, so if you defined macros that used run-pipeline without an explicit starter, this will change the result of new calls to that macro. Basically a hack to be able to set it since I haven’t figured out a better way to do it yet, aside from maybe having people make their own repl modules that set some defaults, and I’m not sure I like that plan.

parameter

(current-prompt-function)  procedure?

(current-prompt-function prompt)  void?
  prompt : procedure?
You can set this parameter to change the prompt. The prompt is responsible for printing the result returned by whatever was last run as well as showing any information you want to see. Right now I would just stick with the default, but I plan on making a nice library of useful functions to put in a prompt (eg. functions for displaying git information, path information with length pruning, ...), and have a way of easily connecting some prompt pieces to make something good (including a way to customize how results are displayed – I want to, for example, have types of objects that can be recognized to print tables nicely, etc).

The given function’s arity is tested to see if it receives various keywords, so that the protocol can be extended and the user only has to specify the keywords that are needed for a given prompt.

Keywords optionally given:

#:last-return-value - fairly self explanatory. If multiple values were returned, they will be given as a list. This will be (void) for the prompt before the first command.

#:last-return-index - This increments once for every command run in the repl. It will be 0 for the prompt before the first command. This is the index that can be used for result-n and return-n.

5 Demo stuff reference

I’ve written various pipeline operators and line macros that I use, but I haven’t decided what should be in the default language yet. So for now they are sitting in a demo directory. But I need some examples. So here I’m documenting a bit.

Use it with (require rash/demo/setup).

pipeline-operator

(=map= arg ...)

Sugar to flatten mapping.

Eg.

|> list 1 2 3 4 =map= + _ 1
is equivalent to
(map (λ (x) (+ x 1)) (list 1 2 3 4))

The _ argument is appended to =map=’s argument list if it is not written explicitly.

pipeline-operator

(=filter= arg ...)

Sugar to flatten filtering.

Eg.

|> list 1 2 3 4 =filter= even?
is equivalent to
(filter (λ (x) (even? x)) (list 1 2 3 4))

The _ argument is appended to =filter=’s argument list if it is not written explicitly.

line-macro

(in-dir directory body)

Dollar and glob expands the directory, then executes the body with current-directory parameterized to the result of expanding directory. If glob expansion returns multiple results, the body is executed once for each of them.

Eg.

in-dir $HOME/projects/* {
  make clean
  make
}

line-macro

(val expression)

Simply returns the expression. This is just to work around not having access to top-level unparenthesized variables.

6 Code and License

The code is available on github.

This library is licensed under the terms of the LGPL version 3, or (at your option) any later version published by the Free Software Foundation (IE LGPL3+).