Wilcox Development Solutions Blog

Error And Exit Code Handling in Rash

February 06, 2025

Rash and Unix Exit Codes

I continue to play with Rash as a language for small shell scripts I know I’ll have to enhance over time. Through it I discovered something interesting about Rash.

In Unix a process returning a non-zero exit code (often called status codes or exit status codes) mean something’s gone wrong. Sometimes the program will print out a message telling you why, sometimes not.

Turns out, when a process returns a non-zero exit code in Rash, Rash will throw a Racket Exception.

My current shell script work involved me using grep on a mass of keywords, then aggregating the result together. However, I forgot that grep returns non-zero exit codes when it can’t find the text you’re looking for. Boom, exception, right in my run.

Solutions

There’s a few different ways to handle this error:

Solution 1: Handle the problem the Racket way

As a new Racket developer I had to go learn Racket exception handling, with seems to be

(with-handlers
    [
        (EXCEPTION-TYPE-OR-LAMBDA-THAT-EVALUATES-TO-TRUE-TO-TRY LAMBDA-TO-EXECUTE-WHEN-THAT-EXCEPTION-TYPE-HAPPENS)
        (ANOTHER-EXCEPTION-TYPE-OR-LAMBDA LAMBDA-TO-EXECUTE-THEN)
    ]
    (the code to execute)
)

Interestingly enough the macro that enables Rash’s ${} syntax (as explored in my previous Rash blog entry), works here, so our code to execute doesn’t have to be a Lisp expression.

(with-handlers ([exn:fail? (lambda (e)
                              (displayln "an error happened, probably string not found")
                              #f)])
    #{ grep "I will not be found" ~/Temp/thefile.txt }
    ;
    ; but it also _can_ be the following:
    (run-pipeline grep "I will not be found" ~/Temp/thefile.txt)
)

This look a little weird, and it does interrupt larger chunks of shell expression with Racket, but may be perfect if you’ve wrapped small bits of shell around larger bits of Racket.

Solution 2: Make up a new keyword - try

Rash’s documentation favors reference documentation over how-to/guide documentation, which means it’s not obvious what Rash can do, sometimes. However, Rash has the ability to create macros that work on top of the shell-alike language defined in Rash. Meaning I can write shell scripts with my own custom keywords.

Here we define a new keyword expression, try/catch, executable in Rash mode, where the catch can trigger other Rash code.


#lang rash

; ---------- define our line macro

(require
 (for-syntax
  racket/base
  syntax/parse))

(define-line-macro try
  (syntax-parser [(_ body (~datum catch) catch-body)
    #'(with-handlers ([(lambda (e) #t) (lambda (e) catch-body)])
        body)]))

; ----- the magic starts here, folks ----------------

try {
  grep "I will not be found" ~/Temp/thefile.txt
} catch {
  echo "an error happened, probably string not found"
}

Almost all of this code is extracted from a Rash demo file, but that file conflates this simple example with some other things.

The define-line-macro quickly gets too complicated for me to parse, but it seems to plug appropriate parts of the expression into appropriate parts in with-handlers, so coooooooooool.

And it works - my error line IS executed!

Rash and the amazing technicolor language dream-coat

A shell language where I can invent my own keywords sounds pretty neat. Practically, most of my Rash scripts tend to be light on the Rash/shell and heavy on the Racket, and custom keywords means I’ll be less able to share things across the fence with my Bash using friends, but did you hear the part about my own keywords??!!