A Nim mini DSL to execute shell commands

library, macro, dsl, shell
nimble install shell



A mini Nim DSL to execute shell commands more conveniently.


With this macro you can simply write

  touch foo
  mv foo bar
  rm bar

which is then rewritten to something equivalent to:

execShell("touch foo")
execShell("mv foo bar")
execShell("rm bar")

where execShell is a proc around startProcess for normal compilation and `gorgeEx` when using NimScript.

See Full expansion of the macro below for more details and how to read the exit code of executed commands.

Most simple things should work as expected. See below for some known quirks.

one and pipe

By default each line in the shell macro will be handled by a different call to execShell. If you need several commands, which depend on the state of the previous, you may do so via the one command like so:

    mkdir foo
    cd foo
    touch bar
    cd ".."
    rm foo/bar

Similar to the one command, the pipe command exists. This concats the command via the shell pipe |:

    cat test.txt
    head -3

will produce:

execShell("cat test.txt | head -3")

Both of these can even be combined!

    mkdir foo
    pushd foo
    echo "Hallo\nWorld" > test.txt
      cat test.txt
      grep H
    rm foo/test.txt
    rmdir foo

will work just as expected, echoing Hallo in the shell.

Accented quotes

Accented quotes allow you to do two different things. Raw strings and Nim symbol quoting.

Note: this has the downside of disallowing ` as a token to be handed to the shell. If you want to use the shell’s `, you need to put the appropriate command into quotation marks.

Raw strings

If you want to hand a literal string to the shell, you may do so by putting it into accented quotes:

echo `hello`

will be rewritten to

execShell("echo \"hello\"")

For a string consisting of multiple commands / words, put quotation marks around it:

echo `"Hello from Nim!"`

which will then also be rewritten to:

execShell("echo \"Hello from Nim!\"")

Nim symbol quoting

Another important feature to make this library useful is quoting of Nim symbols. In order to support this, put the Nim symbol into accented quotes and in addition prefix it by $ as:

let name = "Vindaar"
  echo Hello from `$name`

which will perform the call:

execShell(&"echo Hello from {name}!")

and after the call to strformat.&:

execShell("echo Hello from Vindaar!")

Assignment of results to Nim variables

Also useful is assignment of the result of a shell call to a Nim string. This can be done with the shellAssign macro. It is a little special compared to the shell and shellEcho macros. It only supports a single statement (*), which needs to be an assignment of a shell call of the syntax presented above to a Nim variable, such as:

var name = ""
  name = echo Araq
assert name == "Araq"

Here the left name is the Nim variable (note: this is an exception of the Nim symbol quoting mentioned above!), whereas the right hand side is an arbitrary shell call, in this case a simple call to echo. The Nim variable will be assigned the result of the shell call, by being rewritten to:

var name = ""
name = asgnShell("echo Araq")
assert name == "Araq"

asgnShell is internally called by execShell mentioned above. asgnShell itself performs the calls to execCmdEx (or exec for NimScript).

(*): a single statement is not entirely precise, because the one and pipe operators can be used in combination with the assignment! For example the following is also possible:

var res = ""
  res = pipe:
    seq 0 1 10
    tail -3
assert res == "8\n9\n10"


This macro can also be used in NimScript! Instead of execCmdEx the nimscript.exec is used.

Known issues

Certain things unfortunately have to go into quotation marks. As seen in the one example above, the simple .. is not allowed.

Variable assignments in the shell need to be handed via a string literal:

    "a=`echo hello`"
    echo $a

Also if you need assignment via ‘:’ or ‘=’, put it also in quotation marks. Say you wish to compile a Nim program, you might want to do:

  nim c "--out:noTest" test.nim

In general, if in doubt you can just write strings or triple string (to pass a =”= to the shell).

Full expansion of the macro

As mentioned at the top of the README, the expansion shown is simplified (as a matter of fact it was as simple once, but has since become more complex).

The full expansion of the first example is:

discard block:
  var outputStr381052 = ""
  var exitCode381051: int
  if exitCode381051 ==
    let tmp381063 = execShell("touch foo")
    outputStr381052 = outputStr381052 &
    exitCode381051 = tmp381063[1]
    echo "Skipped command `" & "touch foo" &
        "` due to failure in previous command!"
  if exitCode381051 ==
    let tmp381064 = execShell("mv foo bar")
    outputStr381052 = outputStr381052 &
    exitCode381051 = tmp381064[1]
    echo "Skipped command `" & "mv foo bar" &
        "` due to failure in previous command!"
  if exitCode381051 ==
    let tmp381065 = execShell("rm bar")
    outputStr381052 = outputStr381052 &
    exitCode381051 = tmp381065[1]
    echo "Skipped command `" & "rm bar" &
        "` due to failure in previous command!"
  (outputStr381052, exitCode381051)

As can be seen from the expansion above, successive commands are only run, if the exit code of the previous command was 0, while the output is appended to the previous command’s output.

The normal shell command discards the return value of the block. If you want to keep it, use the shellVerbose macro:

let res = shellVerbose:

where res will be of type tuple[output: string, exitCode: string] according to the expansion above.


In order to see what’s going on, you can either compile your program with the -d:debugShell flag, which will then echo the rewritten commands during compilation. Alternatively in order to avoid calling the commands immediately, you may use the shellEcho macro instead. It simply echoes the commands that would otherwise be run.