How to execute Rust code directly on Unix systems? (using the shebang)

4.5k views Asked by At

From reading this thread, it looks like its possible to use the shebang to run Rust *.

#!/usr/bin/env rustc

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

Making this executable and running does compile, but not run the code.

chmod +x hello_world.rs
./hello_world.rs

However this only compiles the code into hello_world.

Can *.rs files be executed directly, similar to a shell script?


* This references rustx, I looked into this, but its a bash script which compiles the script every time (without caching) and never removes the file from the temp directory, although this could be improved. Also it has the significant limitation that it can't use crates.

5

There are 5 answers

2
DK. On BEST ANSWER

There's cargo-script. That also lets you use dependencies.

After installing cargo-script via cargo install cargo-script, you can create your script file (hello.rs) like this:

#!/usr/bin/env run-cargo-script

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

To execute it, you need to:

$ chmod +x hello.rs
$ ./hello.rs
   Compiling hello v0.1.0 (file://~/.cargo/.cargo/script-cache/file-hello-d746fc676c0590b)
    Finished release [optimized] target(s) in 0.80 secs
Hello World!

To use crates from crates.io, please see the tutorial in the README linked above.

2
wimh On

This seems to work:

#!/bin/sh
//usr/bin/env rustc $0 -o a.out && ./a.out && rm ./a.out ; exit

fn main() {
    println!("Hello World!");
}
0
i.petruk On

I have written a tool just for that: Scriptisto. It is a fully language agnostic tool and it works with other compiled languages or languages that have expensive validation steps (Python with mypy).

For Rust it can also fetch dependencies behind the scenes or build entirely in Docker without having a Rust compiler installed. scriptisto embeds those templates into the binary so you can bootstrap easily:

$ scriptisto new rust > ./script.rs
$ chmod +x ./script.rs
$ ./script.rs

Instead of new rust you can do new docker-rust and the build will not require Rust compiler on your host system.

0
HappyFace On
#!/bin/sh
#![allow()] /*
            exec cargo-play --cached --release $0 -- "$@"
                        */

Needs cargo-play. You can see a solution that doesn't need anything here:

#!/bin/sh
#![allow()] /*

# rust self-compiler by Mahmoud Al-Qudsi, Copyright NeoSmart Technologies 2020
# See <https://neosmart.net/blog/self-compiling-rust-code/> for info & updates.
#
# This code is freely released to the public domain. In case a public domain
# license is insufficient for your legal department, this code is also licensed
# under the MIT license.

# Get an output path that is derived from the complete path to this self script.
# - `realpath` makes sure if you have two separate `script.rs` files in two
#   different directories, they get mapped to different binaries.
# - `which` makes that work even if you store this script in $PATH and execute
#   it by its filename alone.
# - `cut` is used to print only the hash and not the filename, which `md5sum`
#   always includes in its output.
OUT=/tmp/$(printf "%s" $(realpath $(which "$0")) | md5sum | cut -d' '  -f1)

# Calculate hash of the current contents of the script, so we can avoid
# recompiling if it hasn't changed.
MD5=$(md5sum "$0" | cut -d' '  -f1)

# Check if we have a previously compiled output for this exact source code.
if !(test -f "${OUT}.md5" && test "${MD5}" = "$(cat ${OUT}.md5)"); then
    # The script has been modified or is otherwise not cached.
    # Check if the script already contains an `fn main()` entry point.
    if grep -Eq '^\s*(\[.*?\])*\s*fn\s*main\b*' "$0"; then
        # Compile the input script as-is to the previously determined location.
        rustc "$0" -o ${OUT}
        # Save rustc's exit code so we can compare against it later.
        RUSTC_STATUS=$?
    else
        # The script does not contain an `fn main()` entry point, so add one.
        # We don't use `printf 'fn main() { %s }' because the shebang must
        # come at the beginning of the line, and we don't use `tail` to skip
        # it because that would result in incorrect line numbers in any errors
        # reported by rustc, instead we just comment out the shebang but leave
        # it on the same line as `fn main() {`.
        printf "fn main() {//%s\n}" "$(cat $0)" | rustc - -o ${OUT}
        # Save rustc's exit code so we can compare against it later.
        RUSTC_STATUS=$?
    fi

    # Check if we compiled the script OK, or exit bubbling up the return code.
    if test "${RUSTC_STATUS}" -ne 0; then
        exit ${RUSTC_STATUS}
    fi

    # Save the MD5 of the current version of the script so we can compare
    # against it next time.
    printf "%s" ${MD5} > ${OUT}.md5
fi

# Execute the compiled output. This also ends execution of the shell script,
# as it actually replaces its process with ours; see exec(3) for more on this.
exec ${OUT} $@

# At this point, it's OK to write raw rust code as the shell interpreter
# never gets this far. But we're actually still in the rust comment we opened
# on line 2, so close that: */
0
MorganGalpin On

While the other answers are excellent, I wanted a simple way to compile, cache and run a standalone script. My reasoning being that if I'm distributing a script that depends on rust being installed, I probably can't also depend on some third-party library also being installed in order for it to be compiled.

If I'm going to go to the trouble of passing around multiple files, I might as well just pass around a precompiled binary. If my use case for the script/program is complex enough, then I might as well go through the standard cargo build process in a git repo.

So for the single file that only depends on rust and the standard library, use a hello.rs file like the following:

#!/bin/sh
//bin/bash -ec '[ "$0" -nt "${0%.*}" ] && rustc "$0" -o "${0%.*}"; "${0%.*}" "$@"' "$0" "$@"; exit $?

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() > 1 {
        println!("Hello {}!", &args[1]);
    } else {
        println!("Hello world!");
    }
}

To help grok what the shebang is doing, try this one instead. It does the same thing, but is easier to maintain:

#!/bin/sh
//bin/bash -exc 'source_file="$0"; exe_file="${source_file%.*}"; [ "$source_file" -nt "$exe_file" ] && rustc "$source_file" -o "$exe_file"; "$exe_file" "$@"' "$0" "$@"; exit $?

This solution is based on the solution by wimh, but has the following additional features:

  1. Caches the compiled script, aka the program, in the same directory as the script. This also works when the script's directory is added to the path.
  2. Passes the script's command line arguments to the program.
  3. The script's exit code is the same exit code as the program, or the compiler.

Note: the shebang script relies on the script file having some kind of file suffix, e.g. .rs or .sh. Without that, the compiler will complain about overwriting the script file with the generated executable.

My testing shows the script adds about 10ms of overhead versus running the compiled program directly.

Edit: There's an RFC in progress to add a solution similar to Scriptisto to the rust core to make a standard way to solve the OP's problem.