Run.sh – Task organisation for dev projects, based on a pure shell script

4 months ago 9

Task organisation for dev projects,
based on a pure shell script.

 Github

Get started

Download
(Latest version of task runner)

  1. Download task runner
  2. Make it executable, e.g. chmod +x run
  3. Put into path, e.g. mv run /usr/local/bin/run

Needs Bash 3.2+, tested on Linux and macOS.

How it works

Place a task file called run.sh in your project root and make it executable. Let’s say your project is a then your task file could look like this:


run::compile() {
go build \
-o out/ \
src/main.go
}


run::install() {
go get -t ./...
go mod tidy
}

run::bundle() {
./node_modules/.bin/esbuild \
src/index.ts \
--bundle \
--outfile=dist/app.js
}


run::install() {
npm install --strict-peer-deps
}

run::server() {
PORT=8000 \
CONFIG=/app/config.yml \
python src/server.py
}


run::install() {
pip install \
-r requirements.txt
}

A run.sh task file is a plain regular shell script. It behaves exactly as you would expect from any other shell script. There is no special magic to it, except that it adheres to the following convention:

  • The task definitions are shell commands (functions) that carry a run:: prefix.
  • Tasks can optionally be preceded by a descriptive comment. The first comment line is interpreted as title, and the following lines as additional info text.

These notation rules allow the run task runner to recognise and process the tasks.

Run a task

For example, if you want to execute the install task:

$ run install

Under the hood, the task runner evaluates the task file in a bash subprocess and then invokes the task with the respective name – in this case, the bash function run::install.

Any additional CLI arguments will be passed on to the task as is.

List all available tasks

List all available tasks along with their title:

$ run --list

If a task has additional lines of commentary below the title, you can print that by running e.g. run --info install.

Usage without task runner

Without the task runner available (e.g. when in a CI or production environment), you can still access your tasks by sourcing the run.sh task file and then directly calling the task commands by their original names.

$ . run.sh
$ run::install

(The task runner effectively does the same thing.)

Why?

  • Provide a set of well-groomed and well-defined tasks as entrypoints to your project – think of it as friendly API for developers to interact with your project.
  • The run.sh file structure is simple, discoverable, and self-documenting. It’s also agnostic of the project’s main language.
  • Re-use the very same command configurations on all stages, from development over CI to production.
  • What could be more straightforward than storing shell commands in shell scripts? Shell script might not be the nicest language on earth, but it’s powerful and widely adopted. (And when you work on the command line, you are knee-deep into shell script land anyway.)
  • The task runner tool is optional: you neither force it onto everyone else, nor do you have to roll it out to all stages.

Shell Cheat Sheet

Here are a few handy shell scripting snippets.

Variable with default fallback value

“Ternary” assignment

PORT=$([[ "$IS_PROD" ]] && echo 443 || echo 8080)

Task input args

run::hello() {
echo "$1"
echo "$2"
}

Pass on all task input args

run::print() {
echo "$@"
}

Call other task

run::hello() {
run::other-task
}

Check, if…

[[ -f file.txt ]]
[[ -d folder/ ]]
[[ -z "$VAR" ]]
[[ -n "$VAR" ]]

Read file contents into variable

Include other shell file

(The path is relative to the shell’s current working directory.)

Get absolute path of script’s location

THIS_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)

(This declaration must be placed at top level.)

Use .env files

source .env
[[ -f .env ]] && source .env
[[ -f .env ]] && source .env || source .env.dev

Exit immediately on failure

Read Entire Article