make is a utility for automating builds. You specify the source and the build file and make will determine which file(s) have to be re-built. Using this functionality in make as an all-round tool for command running as well, is considered common practice. Yes, you could write Shell scripts for this instead and they would be probably equally good. But using make has its own charm (and gets you karma points).

Even this Blog has a Makefile file in its root directory that helps with creating posts, linting posts and debugging all kind of problems.

Running the command make in the root directory of this blog displays an overview of the available targets (as they are called in the make-world).

$ make
Available targets:
help               Show a list of available targets.
image_docker       Built the docker container
image_podman       Built the podman container
lint_all           Lint all posts with markdown-lint(mdl) and markdown-link-check(mlc)
lint_post          Lint a specific post. `make lint_post post=$filepath`
new_post           Create a new and empty post: make new_post
preview_docker     Run the blog with docker.
preview_podman     Run the blog with podman

This is very useful for writing posts and stuff and ensures it is easy for everyone to post to this blog.

Make standard

They way make works is, that it looks for a file Makefile in the current directory, parses it and executes the defined targets.

A simple example would look like this:

$ cat Makefile
bar: foo.c
  cc foo.c -o bar

The first line contains the target bar, followed by the dependency foo.c. Compiling the file foo.c with the output set, will then create the file bar (the target).

If we run it with (or without) the target name, this would be the output:

# with the target name
$ make bar
cc foo.c -o bar

Running it a second time will not do anything:

$ make bar
make: 'bar' is up to date.

The file bar has already been created and since the file foo.c has not change. the build process does not need to be run again. In software development this is mainly used to compile the source code into binary files (or whatever is needed) and to perform all kind of tasks.

Make non-standard

We can use this functionality to mis-use make as a command runner as well.

With a different target definition we can run other commands and just skip the dependencies.

$ cat Makefile
   @echo "Hello world!"

Running this Makefile using make, we can output “Hello world!” or run any other command. If we take the target image_podman from the Makefile of the Techblog project, its structure is very simple.

  buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .

Just running make with this target will then create a Podman image for further testing and we don’t have to deal with the whole command anymore.

$ make image_podman
buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .

But there are some limitations when using make in general.

  1. Setting a variable is not straight forward and requires a different syntax than eg. when using bash.
  2. Variables cannot be used between multiple lines/commands within the same target. Setting a variable in one line will not make it available in any following line. There are workarounds, though.
  3. The file Makefile requires hard tabs to be present when defining the commands below the target name. These are usually not visible and you have to know that in order to make make make thinks (fun intended, sry).
  4. The syntax for variables is a little bit awkward. You have to mark them with a double dollar sign ($$) instead of a single one like e.g. in bash.
  5. A lot of other “specialities”.

Some of these characteristics are there for historically reasons, others just because. But using make as command runner while its original intention is to actually build stuff based on dependencies must be expected to come with some form of punishment.

Just command runner

As it turns out using make like this has become so popular, that other projects started developing tools for that purpose only.

The tool just is just one of them (HAHA!), but it stands for a number of tools with similar goals (see the list at the bottom of this post).

To make it easy it is already in most Linux repositories available and can be quickly installed.

Then, similar to make, you define your commands in a special file: .justfile (there are alternatives).

In the case of just, the syntax looks quite similar to what you have seen from make so far.

# justfile
  buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .

The invocation looks quite similar too:

$ just image_podman

Though I probably would choose a more fitting target name instead.

The feature of make to be able to add dependencies is also possible in just. But rather on depending on the state of files in the file system (like a building system like make requires), the dependencies of the command-runner effect other recipes.

You could e.g. extend the image building process by adding a recipe to run the latest image after building it.

# .justfile
  buildah bud --build-arg UID=${UID} -t techblog -f .dev/Dockerfile .

run_techblog: image_podman
  podman run --rm -it localhost/techblog -t techbog

Running now just run_techblog will first build the image again for Podman and then start the image once the building process is completed.


If just does not make anything different than make, why consider it instead?

The examples were showing only the similarities. The differences between make and just are as big/small as you would expect from a building tool versus a command runner. An (incomplete) list:

  1. Remember the phony section in every Makefile to define non-file based targets in make. Basically the feature that always defines a target as out-of-date and often abused for defaults? Well this is already builtin in just

    # Output all targets
      just --list
  2. Each command in make is treated separately. In order to pass variables from on command as input or parameter into the next line in a recipe, you have to abide certain rules, which make the life harder than necessary. What makes sense in make hinders a command-runner. There it is allowed to not only pass variables, but also define a rule (the commands that make or just run) just as a shell script instead. Including all usual environment variables.

  3. just runs from any sub-directory down the directory path that contains a just-file. While make only search the current directory for its Makefile, just reverses up the directory path until it finds its file. make is a bit more noisy in that case and just flat out refuses to do anything, if it cannot find its Makefile file.


Here is a list of alternatives to just and make, partly copied from the Readme file of just and extended with more entries. There is certainly no shortage on building tools and command runners for every platform.

  • aap: A recipe based tool for building and running commands.
  • cargo-make: A command runner for Rust projects.
  • doit: A Python based command-runner/building-tool.
  • gulp: A Javascript based building framework.
  • haku: A make-like command runner written in Rust.
  • maid: A Markdown-based command runner written in JavaScript.
  • make: The Unix build tool that inspired just. There are a few different modern day descendents of the original make, including FreeBSD Make and GNU Make.
  • makesure: A simple and portable command runner written in AWK and shell.
  • mask: A Markdown-based command runner written in Rust.
  • microsoft/just: A JavaScript-based command runner written in JavaScript.
  • mmake: A wrapper around make with a number of improvements, including remote includes.
  • rake: A Make-like program implemented in Ruby.
  • robo: A YAML-based command runner written in Go.
  • scons: Cross-platform software construction tool.
  • snakemake: Data-analysis tool that can be used like a command-runner.
  • task: A YAML-based command runner written in Go.
  • tup: A cross-platform file-based build system.
  • waf: A building system.

I did not see any killer-feature (yet), that would let me recommend one tool over the other. But you see a lot of them flying around in the FOSS world and it certainly cannot hurt to get acquainted.

Daniel Buøy-Vehn

Senior Systems Consultant at Redpill Linpro

Daniel works with automation in the realm of Ansible, AWX, Tower, Terraform and Puppet. He rolls out mainly to our customer in Norway to assist them with the integration and automation projects.

Alarms made right

At my work, we’re very much dependent on alarms. The systems need to be operational 24/7. When unexpected issues arise, timely manual intervention may be essential. We need good monitoring systems to catch whatever is wrong and good alarm systems to make someone aware that something urgently needs attention. I would claim that we’re quite good at setting up, tuning, and handling alarms.

When I’m not at work, I’m often sailing, often single-handedly for longer distances. Alarms are important for ... [continue reading]

Containerized Development Environment

Published on February 28, 2024


Published on February 27, 2024