Rust Temptation

Get to coding faster, with a pre-setup that includes all of boilerplate and workflows of a modern software project.

Read the Docs

just

Note

Most of the workflows in this project utilize the just command runner. I highly recommend reading the offical documentation to get a feel for how to read the current "recipes" in a justfile.

Features

just has a ton of useful features, and many improvements over make:

  • just is a command runner, not a build system, so it avoids much of make's complexity and idiosyncrasies. No need for .PHONY recipes!

  • Linux, MacOS, and Windows are supported with no additional dependencies. (Although if your system doesn't have an sh, you'll need to choose a different shell.)

  • Errors are specific and informative, and syntax errors are reported along with their source context.

  • Recipes can accept command line arguments.

  • Wherever possible, errors are resolved statically. Unknown recipes and circular dependencies are reported before anything runs.

  • just loads .env files, making it easy to populate environment variables.

  • Recipes can be listed from the command line.

  • Command line completion scripts are available for most popular shells.

  • Recipes can be written in arbitrary languages, like Python or NodeJS.

  • just can be invoked from any subdirectory, not just the directory that contains the justfile.

  • And much more!

Avoiding make's Complexity and Idiosyncrasies

make has some behaviors which are confusing, complicated, or make it unsuitable for use as a general command runner.

One example is that under some circumstances, make won't actually run the commands in a recipe. For example, if you have a file called test and the following makefile:

test:
  ./test

make will refuse to run your tests:

$ make test
make: `test' is up to date.

make assumes that the test recipe produces a file called test. Since this file exists and the recipe has no other dependencies, make thinks that it doesn't have anything to do and exits.

To be fair, this behavior is desirable when using make as a build system, but not when using it as a command runner. You can disable this behavior for specific targets using make's built-in .PHONY target name

but the syntax is verbose and can be hard to remember. The explicit list of phony targets, written separately from the recipe definitions, also introduces the risk of accidentally defining a new non-phony target. In just, all recipes are treated as if they were phony.

Other examples of make's idiosyncrasies include the difference between = and := in assignments, the confusing error messages that are produced if you mess up your makefile, needing $$ to use environment variables in recipes, and incompatibilities between different flavors of make.

Choosing a Different Shell

The shell setting controls the command used to invoke recipe lines and backticks. Shebang recipes are unaffected. The default shell is sh -cu.

# use python3 to execute recipe lines and backticks
set shell := ["python3", "-c"]

# use print to capture result of evaluation
foos := `print("foo" * 4)`

foo:
  print("Snake snake snake snake.")
  print("{{foos}}")

just passes the command to be executed as an argument. Many shells will need an additional flag, often -c, to make them evaluate the first argument.

Windows Shell

just uses sh on Windows by default. To use a different shell on Windows, use windows-shell:

set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]

hello:
  Write-Host "Hello, world!"

See powershell.just for a justfile that uses PowerShell on all platforms.

Windows PowerShell

set windows-powershell uses the legacy powershell.exe binary, and is no longer recommended. See the windows-shell setting above for a more flexible way to control which shell is used on Windows.

just uses sh on Windows by default. To use powershell.exe instead, set windows-powershell to true.

set windows-powershell := true

hello:
  Write-Host "Hello, world!"

Python 3

set shell := ["python3", "-c"]

Bash

set shell := ["bash", "-uc"]

Z Shell

set shell := ["zsh", "-uc"]

Fish

set shell := ["fish", "-c"]

Nushell

set shell := ["nu", "-c"]

If you want to change the default table mode to light:

set shell := ['nu', '-m', 'light', '-c']

Nushell was written in Rust, and has cross-platform support for Windows / macOS and Linux.

Recipes with Arguments

just supports a number of useful command line options for listing, dumping, and debugging recipes and variables:

$ just --list
Available recipes:
  js
  perl
  polyglot
  python
  ruby
$ just --show perl
perl:
  #!/usr/bin/env perl
  print "Larry Wall says Hi!\n";
$ just --show polyglot
polyglot: python js perl sh ruby

Some command-line options can be set with environment variables. For example:

export JUST_UNSTABLE=1
just

Is equivalent to:

just --unstable

Consult just --help to see which options can be set from environment variables.

Loading .env Files

If any of dotenv-load, dotenv-filename, dotenv-path, or dotenv-required are set, just will try to load environment variables from a file.

If dotenv-path is set, just will look for a file at the given path, which may be absolute, or relative to the working directory.

If dotenv-filename is set just will look for a file at the given path, relative to the working directory and each of its ancestors.

If dotenv-filename is not set, but dotenv-load or dotenv-required are set, just will look for a file named .env, relative to the working directory and each of its ancestors.

dotenv-filename and dotenv-path and similar, but dotenv-path is only checked relative to the working directory, whereas dotenv-filename is checked relative to the working directory and each of its ancestors.

It is not an error if an environment file is not found, unless dotenv-required is set.

The loaded variables are environment variables, not just variables, and so must be accessed using $VARIABLE_NAME in recipes and backticks.

For example, if your .env file contains:

# a comment, will be ignored
DATABASE_ADDRESS=localhost:6379
SERVER_PORT=1337

And your justfile contains:

set dotenv-load

serve:
  @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…"
  ./server --database $DATABASE_ADDRESS --port $SERVER_PORT

just serve will output:

$ just serve
Starting server with database localhost:6379 on port 1337…
./server --database $DATABASE_ADDRESS --port $SERVER_PORT

List Recipes

Recipes can be listed in alphabetical order with just --list:

$ just --list
Available recipes:
    build
    test
    deploy
    lint

Recipes in submodules can be listed with just --list PATH, where PATH is a space- or ::-separated module path:

$ cat justfile
mod foo
$ cat foo.just
mod bar
$ cat bar.just
baz:
$ just --unstable foo bar
Available recipes:
    baz
$ just --unstable foo::bar
Available recipes:
    baz

just --summary is more concise:

$ just --summary
build test deploy lint

Pass --unsorted to print recipes in the order they appear in the justfile:

test:
  echo 'Testing!'

build:
  echo 'Building!'
$ just --list --unsorted
Available recipes:
    test
    build
$ just --summary --unsorted
test build

If you'd like just to default to listing the recipes in the justfile, you can use this as your default recipe:

default:
  @just --list

Note that you may need to add --justfile {{justfile()}} to the line above. Without it, if you executed just -f /some/distant/justfile -d . or just -f ./non-standard-justfile, the plain just --list inside the recipe would not necessarily use the file you provided. It would try to find a justfile in your current path, maybe even resulting in a No justfile found error.

The heading text can be customized with --list-heading:

$ just --list --list-heading $'Cool stuff…\n'
Cool stuff…
    test
    build

And the indentation can be customized with --list-prefix:

$ just --list --list-prefix ····
Available recipes:
····test
····build

The argument to --list-heading replaces both the heading and the newline following it, so it should contain a newline if non-empty. It works this way so you can suppress the heading line entirely by passing the empty string:

$ just --list --list-heading ''
    test
    build

Command Line Completion

Shell completion scripts for Bash, Elvish, Fish, Nushell, PowerShell, and Zsh are available release archives.

The just binary can also generate the same completion scripts at runtime using just --completions SHELL:

just --completions zsh > just.zsh

Please refer to your shell's documentation for how to install them.

macOS Note: Recent versions of macOS use zsh as the default shell. If you use Homebrew to install just, it will automatically install the most recent copy of the zsh completion script in the Homebrew zsh directory, which the built-in version of zsh doesn't know about by default. It's best to use this copy of the script if possible, since it will be updated whenever you update just via Homebrew. Also, many other Homebrew packages use the same location for completion scripts, and the built-in zsh doesn't know about those either. To take advantage of just completion in zsh in this scenario, you can set fpath to the Homebrew location before calling compinit. Note also that Oh My Zsh runs compinit by default. So your .zshrc file could look like this:

# Init Homebrew, which adds environment variables
eval "$(brew shellenv)"

fpath=($HOMEBREW_PREFIX/share/zsh/site-functions $fpath)

# Then choose one of these options:
# 1. If you're using Oh My Zsh, you can initialize it here
# source $ZSH/oh-my-zsh.sh

# 2. Otherwise, run compinit yourself
# autoload -U compinit
# compinit

Using Other Languages

Recipes that start with #! are called shebang recipes, and are executed by saving the recipe body to a file and running it. This lets you write recipes in different languages:

polyglot: python js perl sh ruby nu

python:
  #!/usr/bin/env python3
  print('Hello from python!')

js:
  #!/usr/bin/env node
  console.log('Greetings from JavaScript!')

perl:
  #!/usr/bin/env perl
  print "Larry Wall says Hi!\n";

sh:
  #!/usr/bin/env sh
  hello='Yo'
  echo "$hello from a shell script!"

nu:
  #!/usr/bin/env nu
  let hello = 'Hola'
  echo $"($hello) from a nushell script!"

ruby:
  #!/usr/bin/env ruby
  puts "Hello from ruby!"
$ just polyglot
Hello from python!
Greetings from JavaScript!
Larry Wall says Hi!
Yo from a shell script!
Hola from a nushell script!
Hello from ruby!

On Unix-like operating systems, including Linux and MacOS, shebang recipes are executed by saving the recipe body to a file in a temporary directory, marking the file as executable, and executing it. The OS then parses the shebang line into a command line and invokes it, including the path to the file. For example, if a recipe starts with #!/usr/bin/env bash, the final command that the OS runs will be something like /usr/bin/env bash /tmp/PATH_TO_SAVED_RECIPE_BODY.

Shebang line splitting is operating system dependent. When passing a command with arguments, you may need to tell env to split them explicitly by using the -S flag:

run:
  #!/usr/bin/env -S bash -x
  ls

Windows does not support shebang lines. On Windows, just splits the shebang line into a command and arguments, saves the recipe body to a file, and invokes the split command and arguments, adding the path to the saved recipe body as the final argument. For example, on Windows, if a recipe starts with #! py, the final command the OS runs will be something like py C:\Temp\PATH_TO_SAVED_RECIPE_BODY.

Safer Bash Shebang Recipes

If you're writing a bash shebang recipe, consider adding set -euxo pipefail:

foo:
  #!/usr/bin/env bash
  set -euxo pipefail
  hello='Yo'
  echo "$hello from Bash!"

It isn't strictly necessary, but set -euxo pipefail turns on a few useful features that make bash shebang recipes behave more like normal, linewise just recipe:

  • set -e makes bash exit if a command fails.

  • set -u makes bash exit if a variable is undefined.

  • set -x makes bash print each script line before it's run.

  • set -o pipefail makes bash exit if a command in a pipeline fails. This is bash-specific, so isn't turned on in normal linewise just recipes.

Together, these avoid a lot of shell scripting gotchas.

mdbook

Introduction

mdBook is a command line tool to create books with Markdown. It is ideal for creating product or API documentation, tutorials, course materials or anything that requires a clean, easily navigable and customizable presentation.

Features

  • mdbook-admonish

    Responsible for the "fancy" views of the documentation. This current tip is an example of one.