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.”
I made a quick demo recording of an interactive repl here.
Also I gave a talk at RacketCon 2017 about it, which can be viewed here.
Rash is not entirely stable. It’s still missing features, and there are some names and APIs I want to re-visit. It’s in active development.
However, I use it as my default shell on my laptop, and already like it much better than Bash. I also never launch the Racket repl anymore because Rash does everything the repl does. Try it out, and please report bugs and feedback to me! (Note that at the time of writing, completion and line editing work but are in the readline branch waiting for the next upstream release of the readline package that it relies on.)
Rash is a language and library for shell programming. It provides a nice line-based syntax that make many shell interactions feel similar to other common shells, and provides easy program pipelining by re-providing from Pipeline Macro Library.
Here is a quick example to show what it looks like:
;; the beginning and end of lines act like parens
run-pipeline ls | grep foobar
;; Every line starts with a line-macro.
;; If it doesn't have one, a default is inserted.
;; This default is, by default, run-pipeline.
ls | grep foobar
;; We can make all sorts of interesting piplines that include
;; external processes and racket functions
;; See the run-pipeline docs for more information.
ls |> string-upcase | cowsay
;; Note that the pipe (|) character is normal in the
;; Rash reader, unlike the default Racket reader.
;; In normal Racket, you would type \| or \|>.
;; | is actually short for =unix-pipe=.
;; |> is short for =object-pipe=.
;; By convention, pipeline operators are named with = signs.
;; Because they sort of look like pipes. =I= =guess=.
;; I have made a few handy pipeline operators in the
;; demo directory of the shell-pipeline library.
;; At some point I'll put the most useful ones in the standard
;; exports of the library. But you can also make your own.
;; Inside a pipeline you can use parentheses
;; to escape to Racket.
;; Here we compute the arguments to `ls`.
ls (list '-l (if (even? (random 2)) '-a '-h)) | grep foobar
;; If a line starts with a parenthesis, it is
;; treated as a normal Racket form -- no line macro
(define (add6 x) (+ 6 x))
;; This creates one potential ambiguity:
;; If you really want parens at the start of a
;; line with an implicit line macro.
;; For example, here we want to compute which
;; compiler to use:
;; (if use-clang? 'clang 'gcc) -o foo foo.c
;; We can use another type of paren, like a bracket
;; or brace. It's a bit of an ugly hack, but
;; the convenience of allowing parens to make
;; normal racket forms is worth it.
(define use-clang? (even? (random 2)))
[if use-clang? 'clang 'gcc] -o foo foo.c
;; But this could potentially change in the future.
;; I'm considering making a line that starts with any type of
;; paren be a non-line-macro line. So you would have to
;; explicitly add a line-macro or the starter pipe.
| (if use-clang? 'clang 'gcc) -o foo foo.c
;; Pipelines can pass objects as well as byte streams.
;; The =unix-pipe= operator returns a port, and the
;; =object-pipe= automatically converts it to a string.
;; But =unix-pipe= has a convenience to add a parser
;; with the #:as flag.
echo "[1, 2, 3]" #:as read-json |> map add1
;; The =unix-pipe= also supports some things you might expect
;; in a Unix shell -- ~ expansion, $ENVIRONMENT_VARIABLE
;; expansion, glob expansion, and $local-variable expansion.
(define my-new-dir "my-new-dir")
cp ~/my-dir/*.rkt $HOME/$my-new-dir/
;; The =unix-pipe= also supports aliases.
(define-simple-pipeline-alias ls 'ls '--color=auto)
;; Now ls has color.
;; If you want to break up a long line, you can comment
;; out the newline like so. I will add \ to escape the
;; newline like other shell languages, but I haven't yet
;; because I think the way I'll add it is gross, but
;; sooner or later I will definitely add it.
ls -laH /sys/class/power_supply/BAT0 | grep now #|
|# |> string-upcase | cowsay
;; If you want to turn one line into two logical lines like
;; this bash snippet: `ls ; cd ..`. too bad.
;; I'm considering adding something to break up a line, but
;; I haven't decided how I want to do it yet.
;; When a pipeline is unsuccessful, an exception is thrown.
;; This when a command in the middle of the pipeline
;; is unsuccessful. There are some flags for controlling
;; the specific behavior.
We can use rash not only by using #lang rash, but also by using the rash macro in any other language:
(rash "ls -l")
The macro reads the string at compile time and turns it into syntax-objects with proper hygiene information, so within the rash code you can refer to variables in scope at the rash use site. But normal strings are inconvenient – they require escaping to write many things, and they don’t provide enough information to get exact location information. I use string delimiters from the udelim package instead:
#lang udelim racket
(rash «ls -l»)
;; These weird strings are especially useful for nesting.
;; You don't have to quote anything, just balance the delimiters.
(rash «cat (rash «which myscript.rkt»)»)
See the udelim docs for more detailed information on the string delimiters (particularly make-string-delim-readtable). The extra delimiters provided by make-udelim-readtable are enabled by default in #lang rash and inside the rash macro.
#lang udelim racket
;; The rash macro is like a `begin`.
(rash «ls -l
;; It splices into definition contexts like begin
(rash «(define foo 5)
;; assume def is a line macro that defines things
;; and uses do-line-macro.
def rktfiles find . -regex rkt$
(+ foo 7)
One convenient thing to do is use other udelim string delimiters that produce #%identifiers.
(define-syntax #%upper-triangles (make-rename-transformer #'rash))
;; Now you can invoke the Rash macro just by using ◸◹ as parens.
;; I am always using this pattern in shells to see what I wrote in my scripts
cat ◸which myscript.rkt◹
The line syntax has no special control flow forms – when I’m ready to write control flow, I’m ready to write a parenthesis. So just use normal Racket control flow forms, and use rash or make-rash-transformer to get back into rash.
(rash «ls -l |> string-upcase»)
(rash «ls -l»))
Since normal Racket code essentially always starts with parens, we can use #lang rash in place of languages that use the normal reader for a lot of things – it’s almost a superset of functionality. The one thing that breaks when switching to #lang rash is identifiers or other non-parenthesized code at the top level of a module. But the only real reason to do that is to print the value of a variable, which can easily be done by wrapping in parens or using a line macro.
(define (foo x)
(* 37.25 x))
(define bar (foo 22))
;; get the value
Can translate to
(define (foo x)
(* 37.25 x))
(define bar (foo 22))
(define-line-macro id (syntax-parser [(_ e) #'e]))
;; or you could do this
;; (values bar)
#lang rash differs from #lang racket in its treatment of top-level expressions. #lang racket prints the result of top-level expressions, but I hate that. #lang rash does not print the value of top-level expressions, so you should explicitly print any racket forms. If #lang rash did print results of top-level expressions, you would get pipeline results (eg. 0 when a unix pipeline is successful), and that could be annoying.
Rash is also useful as an interactive repl that feels like a nice mix between a normal Racket repl and an interactive Bash shell.
(rash options ... codestring)
#: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.
(make-rash-transformer options ...)
TODO - finish make-rash-reader-submodule and document it – it should be like make-rash-transformer only in should essentially create a #lang. Note that the default #lang rash has its input/output/error-output as stdin/stdout/stderr, which is different than the rash macro.
things to document:
(define-line-macro name transformer-expr)
[(_ i:id from-expr body ...+)
#'(for ([i from-expr])
my-for f (list "file1.txt" "file2.txt") ◸rm (id f)◹
(do-line-macro maybe-line-macro arg ...)
TODO - example setting and what is the default.
docs about reader
how to change the inside/outside readtable
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.
(run-pipeline arg ...)
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:
(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)
All the following repl functions are not stable.
(current-prompt-function prompt) → void? prompt : procedure?
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.
I have some proof-of-concept pipes and things in the demo directories of the rash and shell-pipeline repositories. Eventually some of the good stuff from them will probably be improved and moved into the main modules. But you can check them out for ideas of some things you might do.
To use the demo, (require rash/demo/setup). Also look at the file to see some examples.
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+).