Just: Command Runner for Modern Projects
When you start a new project, the first thing you often do is write a handful of shell commands to spin up a dev server, run tests, or bundle assets. Over time, those one‑liners grow into a tangled mess of npm scripts, Makefiles, or ad‑hoc bash snippets. Just steps in as a lightweight, language‑agnostic command runner that brings order without the overhead of a full‑blown task runner. It lets you define concise, reusable tasks in a justfile, supports variables, dependencies, and even parallel execution—all while keeping the learning curve shallow.
Getting Started with Just
Installation is a breeze. On macOS you can use Homebrew, on Linux the package manager of your choice, or grab a pre‑built binary from the releases page. The command below works on most Unix‑like systems:
curl -fsSL https://just.systems/install.sh | sh
Once installed, navigate to your project root and create a justfile. This file is where you declare tasks. A minimal example looks like this:
# justfile
hello:
echo "👋 Welcome to Just!"
Run it with just hello and you’ll see the greeting printed. That’s the entire workflow: define, run, iterate.
Why Choose Just over npm scripts or Make?
- Clarity: Each task lives on its own line with optional documentation.
- Portability: The same
justfileworks on Windows (via WSL) and Linux without modification. - Powerful Features: Variables, conditionals, and parallelism are built‑in.
Because Just is deliberately minimal, you won’t find a massive API surface to learn. Instead, you get a clean DSL that feels like a natural extension of your shell.
Core Concepts and Syntax
Every entry in a justfile follows the pattern task-name [parameters]: followed by an indented block of shell commands. Parameters make tasks reusable, while the colon signals the start of the command block.
build target:
cargo build --release --target {{target}}
The double curly braces {{ }} denote variable interpolation. Variables can be defined globally, per‑task, or passed from the command line.
Defining Variables
Global variables sit at the top of the file and can be overridden by environment variables or CLI flags. Here’s a common pattern for a Python project:
# Global configuration
PYTHON = "python3"
VENV = ".venv"
install:
{{PYTHON}} -m venv {{VENV}}
{{VENV}}/bin/pip install -r requirements.txt
Running just install creates a virtual environment and installs dependencies in one go.
Advanced Task Management
Just shines when tasks depend on one another. You can express dependencies directly in the task signature, ensuring the correct order of execution without manual scripting.
test: lint
{{PYTHON}} -m pytest
lint:
flake8 src/ tests/
When you invoke just test, Just automatically runs lint first. This eliminates the need for a separate “pre‑test” script.
Parallel Execution
Modern CI pipelines often need to run independent jobs simultaneously. Just provides a --jobs flag that lets you spin up multiple tasks in parallel. Combine it with a “group” task to orchestrate complex workflows.
# Run lint, type‑check, and unit tests concurrently
ci:
lint
typecheck
test
# Run the group with 3 workers
just --jobs 3 ci
Each sub‑task runs in its own shell, and Just streams the output with clear prefixes, making debugging straightforward.
Pro tip: Use just --dry-run to preview the exact shell commands that will be executed. It’s a lifesaver when you’re tweaking complex dependency graphs.
Real‑World Use Cases
Below are three scenarios where Just can replace a patchwork of scripts and bring consistency to your workflow.
1. Front‑End Build Pipeline
Imagine a React project that needs to lint, run unit tests, and bundle assets. A typical package.json might contain dozens of npm scripts. With Just, you can consolidate them:
# justfile for a React app
NODE = "node"
NPM = "npm"
install:
{{NPM}} install
lint:
{{NPM}} run lint
test:
{{NPM}} run test
build:
{{NPM}} run build
# Full CI pipeline
ci: install lint test build
Running just ci now performs the entire CI flow with a single command, and the same justfile can be used locally, in Docker, or on a CI server.
2. Data‑Engineering ETL Job
Data pipelines often involve pulling data, transforming it, and loading it into a warehouse. Below is a Python‑centric Just setup that orchestrates a daily ETL run.
# justfile for ETL
PYTHON = "python3"
SCRIPT = "scripts/etl.py"
# Pull raw data from an API
fetch:
{{PYTHON}} {{SCRIPT}} fetch --date {{date}}
# Transform raw data
transform: fetch
{{PYTHON}} {{SCRIPT}} transform --input data/raw/{{date}}.json
# Load into warehouse
load: transform
{{PYTHON}} {{SCRIPT}} load --input data/processed/{{date}}.parquet
# Daily run (default date is today)
run date="{{today}}":
load
Invoke just run to execute the full pipeline for today, or pass a specific date with just run date="2023-12-31". The dependency chain guarantees that each step only runs when its predecessor succeeds.
3. DevOps: Managing Docker Compose Environments
Docker Compose files can become unwieldy when you have multiple environments (dev, staging, prod). Just can abstract the boilerplate:
# justfile for Docker Compose
COMPOSE = "docker compose"
ENV = "dev"
up:
{{COMPOSE}} -f docker-compose.yml -f docker-compose.{{ENV}}.yml up -d
down:
{{COMPOSE}} -f docker-compose.yml -f docker-compose.{{ENV}}.yml down
logs:
{{COMPOSE}} -f docker-compose.yml -f docker-compose.{{ENV}}.yml logs -f
# Switch environment on the fly
set-env env:
export ENV={{env}}
Now you can spin up a development stack with just up, switch to staging by running just set-env env=staging && just up, and view live logs with just logs. No more memorizing long docker compose flags.
Pro tip: Combine Just withdirenvto automatically load environment variables when you cd into the project directory. This makes theENVvariable seamless across shells.
Testing and Debugging Just Tasks
Just includes a built‑in test runner that lets you write assertions directly in the justfile. This is especially handy for validating that your tasks produce the expected output.
# Simple test for the lint task
@test-lint:
output := $(just lint 2>&1)
assert "{{output}}" contains "no issues found"
Run just test-lint and the task will fail with a clear error if the assertion does not hold. You can chain multiple @test-* tasks under a test umbrella to create a comprehensive test suite.
Debugging Tips
- Use
set -xat the top of a task to echo each command before it runs. - Redirect output to a log file with
> logs/task.log 2>&1for post‑mortem analysis. - Leverage Just’s
--summaryflag to get a concise overview of which tasks were executed and their exit codes.
Integrating Just with CI/CD Platforms
Most CI services allow you to run arbitrary shell commands, which means you can drop a just ci call into your pipeline configuration. Here’s a snippet for GitHub Actions:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Just
run: curl -fsSL https://just.systems/install.sh | sh
- name: Run CI pipeline
run: just ci
Because the justfile lives in the repo, any change to the build logic is version‑controlled alongside the code, reducing drift between local development and CI.
Cache Management
Just itself is tiny, but the tasks it runs often generate heavy artifacts (node_modules, compiled binaries, Docker layers). Use the CI platform’s caching mechanisms in conjunction with Just to speed up builds. For example, cache the .venv directory in a Python project:
- name: Cache virtualenv
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ hashFiles('requirements.txt') }}
Now the install task in the justfile will be a near‑no‑op on subsequent runs.
Extending Just with Custom Scripts
While Just’s DSL covers most needs, you can still call out to external scripts for complex logic. The key is to keep the script language‑agnostic and let Just handle orchestration.
# justfile
deploy:
./scripts/deploy.sh {{environment}}
# scripts/deploy.sh
#!/usr/bin/env bash
set -e
env=$1
echo "Deploying to $env..."
# ... more complex deployment steps ...
This pattern separates concerns: Just defines *what* to do, while the Bash (or Python, PowerShell, etc.) script defines *how* to do it. It also makes it easy to reuse the script outside of Just if needed.
Cross‑Platform Considerations
If your team works on both Windows and Unix, write the helper scripts in a portable language like Python. Just will invoke the interpreter correctly, and you avoid shell‑specific quirks.
# justfile
clean:
python scripts/clean.py
# scripts/clean.py
import shutil, pathlib, sys
root = pathlib.Path(__file__).parent.parent
shutil.rmtree(root / "build", ignore_errors=True)
print("✅ Cleaned build artifacts")
Now just clean works identically on all platforms.
Best Practices for Maintaining a Justfile
As your project evolves, the justfile can become a central piece of documentation. Treat it with the same care you would a README or an API spec.
- Group related tasks: Use comments and blank lines to visually separate sections (e.g., “🛠️ Build”, “🧪 Test”).
- Document each task: Place a short comment above the task name; Just will display it when you run
just --list. - Keep tasks atomic: Each task should do one thing well; combine them with dependencies instead of monolithic scripts.
- Version‑lock external tools: Pin versions of linters, formatters, or compilers inside the task to avoid “works on my machine” issues.
Pro tip: Run just --summary regularly to spot tasks that have become slow or flaky. Early detection saves hours of debugging later.
Conclusion
Just offers a pragmatic middle ground between ad‑hoc shell scripts and heavyweight task runners. Its clear syntax, built‑in dependency management, and seamless integration with CI/CD make it a natural fit for modern projects of any size. By consolidating repetitive commands into a well‑structured justfile, teams gain reproducibility, faster onboarding, and a single source of truth for build logic. Whether you’re a solo developer polishing a hobby app or a large organization orchestrating multi‑service deployments, Just can streamline your workflow without imposing a steep learning curve. Give it a try, and let your tasks finally feel just right.