Posted: 2024-03-12
Last modified: 2024-03-12 @ 7e844ff

Rust is a Scripting Language Now

I was talking with a friend, poking fun at a job ad that said they wanted experience in scripting languages such as bash, python, and golang.

After some more banter about compilied languages not being scripting languages, we realized that technically you could make golang seem like a scripting language. All you have to do is have a properly crafted shebang at the top of the file.

And of course I wanted to see if I could do something like that to make Rust seem like a scripting language. After some poking around, here is the final result:

#!/usr/bin/env -S sh -c 'rustc ${0} --out-dir $(dirname ${0}) && ${0%%.rs} ; rm -f ${0%%.rs}'

fn main() {
    println!("Hello world!");
}

Saving that somewhere as a .rs file and running chmod +x on it, you can run it and pretend like it’s a shell script. It will compile into a binary with the same name (sans the .rs suffix), run it, then clean up after itself by removing the binary.

Of course, there are some limitations. Since it’s not a cargo project, there is no way to add dependencies to other crates. Everything is should be contained in this one file. You do have access to the standard library, though.

Quick walkthrough of the shebang

Since a shebang line passes the path to the file it’s contained in as the first argument to the interpreter, we can simply use sh -c "commandstring" [arg] as our interpreter. Inside of the sh command string, ${0} refers to the first argument (the path to our “script”).

So now we can pass ${0} in this subshell context to rustc, making sure to set the --out-dir argument to make sure the binary ends up next to the “script”1. The reason for this is so that the script can be run from anywhere, no matter what your current working directory is. Just type the path and it will run. Or just the script name if the script is in your $PATH!

Then, we run the resulting binary by stripping the .rs suffix from the “script” path, if compilation succeeded (hence &&). Finally, we remove the binary (with -f to suppress any “file not found” errors). We can’t use && or || here, as the script could have different exit codes, and we always want to remove the binary no matter what the exit code was. Hence just a simple ; sequence point.

Should you do all this? Probably not.


  1. An alternative is to pass /tmp as the output directory. Then the cleanup step could be skipped too. Although then the running part would be a bit more complicated. ↩︎