Skip to content
/ run Public

Task runner that helps you easily manage and invoke small scripts and wrappers

License

Notifications You must be signed in to change notification settings

TekWizely/run

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

91 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Run: Easily manage and invoke small scripts and wrappers

GitHub repo size All Contributors GitHub stars GitHub forks Twitter Follow

Do you find yourself using tools like make to manage non build-related scripts?

Build tools are great, but they are not optimized for general script management.

Run aims to be better at managing small scripts and wrappers, while incorporating a familiar make-like syntax.

Runfile

Where make has the ubiquitous Makefile, run has the cleverly-named "Runfile"

By default, run will look for a file named "Runfile" in the current directory, exiting with error if not found.

Read below for details on specifying alternative runfiles, as well as other special modes you might find useful.

Commands

In place of make's targets, runfiles contain 'commands'.

Similar to make, a command's label is used to invoke it from the command-line.

Scripts

Instead of recipes, each runfile command contains a 'script' which is executed when the command is invoked.

You might be used to make's (default) behavior of executing each line of a recipe in a separate sub-shell.

In run, the entire script is executed within a single sub-shell.

TOC


Examples


Simple Command Definitions

Runfile

hello:
  echo "Hello, world"

We'll see that hello shows as an invokable command, but has no other help text.

list commands

$ run list

Commands:
  list       (builtin) List available commands
  help       (builtin) Show help for a command
  version    (builtin) Show run version
  hello

show help for hello command

$ run help hello

hello: no help available.

invoke hello command

$ run hello

Hello, world

Naming Commands

Run accepts the following pattern for command names:

alpha ::= 'a' .. 'z' | 'A' .. 'Z'
digit ::= '0' .. '9'

CMD_NAME ::= [ alpha | '_' ] ( [ alpha | digit | '_' | '-' ] )*

Some examples:

  • hello
  • hello_world
  • hello-world
  • HelloWorld
Case Sensitivity
Invoking Commands

When invoking commands, run treats the command name as case-insensitive:

Runfile

Hello-World:
  echo "Hello, world"

output

$ run Hello-World
$ run Hello-world
$ run hello-world

Hello, world
Displaying Help

When displaying help text, run treats the command name as case-sensitive, displaying the command name as it is defined:

list commands

$ run list

Commands:
  ..
  Hello-World
  ...

show help for Hello-World command

$ run help Hello-World

Hello-World: no help available.
Duplicate Command Names

When registering commands, run treats the command name as case-insensitive, generating an error if a command name is defined multiple times:

Runfile

hello-world:
  echo "Hello, world"

Hello-World:
  echo "Hello, world"

list commands

$ run list

run: duplicate command: hello-world defined on line 4 -- originally defined on line 1

Simple Title Definitions

We can add a simple title to our command, providing some help content.

Runfile

## Hello world example.
hello:
  echo "Hello, world"

output

$ run list

Commands:
  list       (builtin) List available commands
  help       (builtin) Show help for a command
  version    (builtin) Show run version
  hello      Hello world example.
  ...
$ run help hello

hello:
  Hello world example.

Title & Description

We can further flesh out the help content by adding a description.

Runfile

##
# Hello world example.
# Prints "Hello, world".
hello:
  echo "Hello, world"

output

$ run list

Commands:
  list       (builtin) List available commands
  help       (builtin) Show help for a command
  version    (builtin) Show run version
  hello      Hello world example.
  ...
$ run help hello

hello:
  Hello world example.
  Prints "Hello, world".

Arguments

Positional arguments are passed through to your command script.

Runfile

##
# Hello world example.
hello:
  echo "Hello, ${1}"

output

$ run hello Newman

Hello, Newman

Command-Line Options

You can configure command-line options and access their values with environment variables.

Runfile

##
# Hello world example.
# Prints "Hello, <name>".
# OPTION NAME -n,--name <name> Name to say hello to
hello:
  echo "Hello, ${NAME}"

output

$ run help hello

hello:
  Hello world example.
  Prints "Hello, <name>".
Options:
  -h, --help
        Show full help screen
  -n, --name <name>
        Name to say hello to
$ run hello --name=Newman
$ run hello -n Newman

Hello, Newman

Boolean (Flag) Options

Declare flag options by omitting the '<...>' segment.

Runfile

##
# Hello world example.
# OPTION NEWMAN --newman Say hello to Newman
hello:
  NAME="World"
  [[ -n "${NEWMAN}" ]] && NAME="Newman"
  echo "Hello, ${NAME}"

output

$ run help hello

hello:
  Hello world example.
  ...
  --newman
        Say hello to Newman
Setting a Flag Option to TRUE
$ run help --newman=true # true | True | TRUE
$ run help --newman=1    # 1 | t | T
$ run help --newman      # Empty value = true

Hello, Newman
Setting a Flag Option to FALSE
$ run help --newman=false # false | False | FALSE
$ run help --newman=0     # 0 | f | F
$ run help                # Default value = false

Hello, World

Getting -h & --help For Free

If your command defines one or more options, but does not explicitly configure options -h or --help, then they are automatically registered to display the command's help text.

Runfile

##
# Hello world example.
# Prints "Hello, world".
hello:
  echo "Hello, world"

output

$ run hello -h
$ run hello --help

hello:
  Hello world example.
  Prints "Hello, world".

Passing Options Directly Through to the Command Script

If your command does not define any options within the Runfile, then run will pass all command line arguments directly through to the command script.

Runfile

##
# Echo example
# Prints the arguments passed into the script
#
echo:
  echo script arguments = "${@}"

output

$ run echo -h --help Hello Newman

script arguments = -h --help Hello Newman

NOTE: As you likely surmised, help options (-h & --help) are not automatically registered when the command does not define any other options.

What if My Command Script DOES Define Options?

If your command script does define one or more options within the Runfile, you can still pass options directly through to the command script, but the syntax is a bit different:

Runfile

##
# Echo example
# Prints the arguments passed into the script
# Use -- to separate run options from script options
# OPTION ARG -a <arg> Contrived argument
#
echo:
  echo ARG = "${ARG}"
  echo script arguments = "${@}"

output

$ run echo -a my-arg -- -h --help Hello Newman

ARG = my-arg
script arguments = -h --help Hello Newman

Notice the '--' in the argument list - Run will stop parsing options when it encounters the '--' and pass the rest of the arguments through to the command script.


Run Tool Help

Invoking -h or --help with no command shows the help page for the run tool itself.

$ run --help

Usage:
       run <command> [option ...]
          (run <command>)
  or   run list
          (list commands)
  or   run help <command>
          (show help for <command>)
Options:
  -r, --runfile <file>
        Specify runfile (default='${RUNFILE:-Runfile}')
        ex: run -r /my/runfile list
Note:
  Options accept '-' | '--'
  Values can be given as:
        -o value | -o=value
  Flags (booleans) can be given as:
        -f | -f=true | -f=false
  Short options cannot be combined

Using an Alternative Runfile

Via Command Line

You can specify a runfile using the -r | --runfile option:

$ run --runfile /path/to/my/Runfile <command>

NOTE: When specifying a runfile, the file does not have to be named "Runfile".

Via Environment Variables

$RUNFILE

You can specify a runfile using the $RUNFILE environment variable:

$ export RUNFILE="/path/to/my/Runfile"

$ run <command>

For some other interesting uses of $RUNFILE, see:

NOTE: When specifying a runfile, the file does not have to be named "Runfile".

$RUNFILE_ROOTS

You can instruct run to look up the directory path in search of a runfile.

You do this using the $RUNFILE_ROOTS path variable.

  • $RUNFILE_ROOTS is treated as a list of path entries (using standard os path separator)
  • Behaves largely similar to GIT_CEILING_DIRECTORIES
  • If $PWD is a child of a root entry, run walks-up looking for Runfile
  • Roots themselves are generally treated as exclusive (ie not checked)
  • $HOME, if a configured root, is treated as inclusive (ie it is checked)

general usage

export RUNFILE_ROOTS="${HOME}"  # Will walk up to $HOME (inclusively)

most permissive

export RUNFILE_ROOTS="/"  # Will walk up to / (exclusively)

NOTE: $HOME is given special treatment to support the case where a project is given its own user account and lives in the home folder of that user.

For the case of creating globally available tasks, see the Special Modes section.


Runfile Variables

You can define variables within your runfile:

Runfile

NAME := "Newman"

##
# Hello world example.
# Tries to print "Hello, ${NAME}"
hello:
  echo "Hello, ${NAME:-world}"

Local By Default

By default, variables are local to the runfile and are not part of your command's environment.

For example, you can access them within your command's description:

$ run help hello

hello:
  Hello world example.
  Tries to print "Hello, Newman"

But not within your commands script:

$ run hello

Hello, world

Exporting Variables

To make a variable available to your command script, you need to export it:

Runfile

EXPORT NAME := "Newman"

##
# Hello world example.
# Tries to print "Hello, ${NAME}"
hello:
  echo "Hello, ${NAME:-world}"

output

$ run hello

Hello, Newman
Per-Command Variables

You can create variables on a per-command basis:

Runfile

##
# Hello world example.
# Prints "Hello, ${NAME}"
# EXPORT NAME := "world"
hello:
  echo "Hello, ${NAME}"

help output

$ run help hello

hello:
  Hello world example.
  Prints "Hello, world"

command output

$ run hello

Hello, world
Exporting Previously-Defined Variables

You can export previously-defined variables by name:

Runfile

HELLO := "Hello"
NAME  := "world"

##
# Hello world example.
# EXPORT HELLO, NAME
hello:
  echo "${HELLO}, ${NAME}"
Pre-Declaring Exports

You can declare exported variables before they are defined:

Runfile

EXPORT HELLO, NAME

HELLO := "Hello"
NAME  := "world"

##
# Hello world example.
hello:
  echo "${HELLO}, ${NAME}"
Forgetting To Define An Exported Variable

If you export a variable, but don't define it, you will get a WARNING

Runfile

EXPORT HELLO, NAME

NAME := "world"

##
# Hello world example.
hello:
  echo "Hello, ${NAME}"

output

$ run hello

run: WARNING: exported variable not defined: 'HELLO'
Hello, world

Referencing Other Variables

You can reference other variables within your assignment:

Runfile

SALUTATION := "Hello"
NAME       := "Newman"

EXPORT MESSAGE := "${SALUTATION}, ${NAME}"

##
# Hello world example.
hello:
  echo "${MESSAGE}"

Shell Substitution

You can invoke sub-shells and capture their output within your assignment:

Runfile

SALUTATION := "Hello"
NAME       := "$( echo 'Newman )" # Trivial example

EXPORT MESSAGE := "${SALUTATION}, ${NAME}"

##
# Hello world example.
hello:
  echo "${MESSAGE}"

Conditional Assignment

You can conditionally assign a variable, which only assigns a value if one does not already exist.

Runfile

EXPORT NAME ?= "world"

##
# Hello world example.
hello:
  echo "Hello, ${NAME}"

example with default

$ run hello

Hello, world

example with override

NAME="Newman" run hello

Hello, Newman

Assertions

Assertions let you check against expected conditions, exiting with an error message when checks fail.

Assertions have the following syntax:

ASSERT <condition> [ "<error message>" | '<error message>' ]

Note: The error message is optional and will default to "assertion failed" if not provided

Condition

The following condition patterns are supported:

  • [ ... ]
  • [[ ... ]]
  • ( ... )
  • (( ... ))

Note: Run does not interpret the condition. The condition text will be executed, unmodified (including surrounding braces/parens/etc), by the configured shell. Run will inspect the exit status of the check and pass/fail the assertion accordingly.

Assertion Example

Here's an example that uses both global and command-level assertions:

Runfile

##
# Not subject to any assertions
world:
	echo Hello, World

# Assertion applies to ALL following commands
ASSERT [ -n "${HELLO}" ] "Variable HELLO not defined"

##
# Subject to HELLO assertion, even though it doesn't use it
newman:
	echo Hello, Newman

##
# Subject to HELLO assertion, and adds another
# ASSERT [ -n "${NAME}" ] 'Variable NAME not defined'
name:
	echo ${HELLO}, ${NAME}

example with no vars

$ run world

Hello, World

$ run newman

run: ERROR: Variable HELLO not defined

$ run name

run: ERROR: Variable HELLO not defined

example with HELLO

$ HELLO=Hello run newman

Hello, Newman

$ HELLO=Hello run name

run: Variable NAME not defined

example with HELLO and NAME

$ HELLO=Hello NAME=Everybody run name

Hello, Everybody

Note: Assertions apply only to commands and are only checked when a command is invoked. Any globally-defined assertions will apply to ALL commands defined after the assertion.


Invoking Other Commands & Runfiles

.RUN / .RUNFILE Attributes

Run exposes the following attributes:

  • .RUN - Absolute path of the run binary currently in use
  • .RUNFILE - Absolute path of the current Runfile

Your command script can use these to invoke other commands:

Runfile

##
# Invokes hello
# EXPORT RUN := ${.RUN}
# EXPORT RUNFILE := ${.RUNFILE}
test:
    "${RUN}" hello

hello:
    echo "Hello, World"

output

$ run test

Hello, World

Script Shells

Run's default shell is 'sh', but you can specify other shells.

All the standard shells should work.

Per-Command Shell Config

Each command can specify its own shell:

##
# Hello world example.
# NOTE: Requires ${.SHELL}
hello (bash):
  echo "Hello, world"

Global Default Shell Config

You can set the default shell for the entire runfile:

Runfile

# Set default shell for all actions
.SHELL = bash

##
# Hello world example.
# NOTE: Requires ${.SHELL}
hello:
  echo "Hello, world"

Other Executors

You can even specify executors that are not technically shells.

Python Example

Runfile

## Hello world python example.
hello (python):
	print("Hello, world from python!")
Script Execution : env

Run executes scripts using the following command:

/usr/bin/env $SHELL $TMP_SCRIPT_FILE [ARG ...]

Any executor that is on the PATH, can be invoked via env, and takes a filename as its first argument should work.

Custom #! Support

Run allows you to define custom #! lines in your command script:

C Example

Here's an example of running a c program from a shell script using a custom #! header:

Runfile

##
# Hello world c example using #! executor.
# NOTE: Requires gcc
hello:
  #!/usr/bin/env sh
  sed -n -e '7,$p' < "$0" | gcc -x c -o "$0.$$.out" -
  $0.$$.out "$0" "$@"
  STATUS=$?
  rm $0.$$.out
  exit $STATUS
  #include <stdio.h>

  int main(int argc, char **argv)
  {
    printf("Hello, world from c!\n");
    return 0;
  }
Script Execution: Direct

NOTE: The #! executor does not use /user/bin/env to invoke your script. Instead, it attempts to make the temporary script file executable then invoke it directly.


Misc Features

Ignoring Script Lines

You can use a # on the first column of a command script to ignore a line:

Runfile

hello:
    # This comment WILL be present in the executed command script
    echo "Hello, Newman"
# This comment block WILL NOT be present in the executed command script
#   echo "Hello, World"
    echo "Goodbye, now"

Note: Run detects and skips these comment lines when parsing the runfile, so the # will work regardless of what language the script text is written in (i.e even if the target language doesn't support # for comments).


Special Modes

Shebang Mode

In shebang mode, you make your runfile executable and invoke commands directly through it:

runfile.sh

#!/usr/bin/env run shebang

## Hello example using shebang mode
hello:
  echo "Hello, world"

output

$ chmod +x runfile.sh
$ ./runfile.sh hello

Hello, world

Filename used in help text

In shebang mode, the runfile filename replaces references to the run command:

shebang mode help example

$ ./runfile.sh help

Usage:
       runfile.sh <command> [option ...]
                 (run <command>)
  or   runfile.sh list
                 (list commands)
  or   runfile.sh help <command>
                 (show help for <command>)
  ...

shebang mode list example

$ ./runfile.sh list

Commands:
  list           (builtin) List available commands
  help           (builtin) Show help for a command
  run-version    (builtin) Show run version
  hello          Hello example using shebang mode

Version command name

In shebang mode, the version command is renamed to run-version. This enables you to create your own version command, while still providing access to run's version info, if needed.

runfile.sh

#!/usr/bin/env run shebang

## Show runfile.sh version
version:
    echo "runfile.sh v1.2.3"

## Hello example using shebang mode
hello:
  echo "Hello, world"

shebang mode version example

$ ./runfile.sh list
  ...
  run-version    (builtin) Show Run version
  version        Show runfile.sh version
  ...

$ ./runfile.sh version

runfile.sh v1.2.3

$ ./runfile.sh run-version

runfile.sh is powered by run v0.0.0. learn more at https://github.com/TekWizely/run

Main Mode

In main mode you use an executable runfile that consists of a single command, aptly named main:

runfile.sh

#!/usr/bin/env run shebang

## Hello example using main mode
main:
  echo "Hello, world"

In this mode, run's built-in commands are disabled and the main command is invoked directly:

output

$ ./runfile.sh

Hello, world

Filename used in help text

In main mode, the runfile filename replaces references to command name:

main mode help example

$ ./runfile.sh --help

runfile.sh:
  Hello example using main mode

Help options

In main mode, help options (-h & --help) are automatically configured, even if no other options are defined.

This means you will need to use -- in order to pass options through to the main script.


Using direnv to auto-configure $RUNFILE

A nice hack to make executing run tasks within your project more convenient is to use direnv to auto-configure the $RUNFILE environment variable:

create + edit + activate rc file

$ cd ~/my-project
$ direnv edit .

edit .envrc

export RUNFILE="${PWD}/Runfile"

Save & exit. This will activate immediately but will also activate whenever you cd into your project's root folder.

$ cd ~/my-project

direnv: export +RUNFILE

verify

$ echo $RUNFILE

/home/user/my-project/Runfile

With this, you can execute run <cmd> from anywhere in your project.


Installing

Via Bingo

Bingo makes it easy to install (and update) golang apps directly from source:

install

$ bingo install github.com/TekWizely/run

update

$ bingo update run

Pre-Compiled Binaries

See the Releases page as recent releases are accompanied by pre-compiled binaries for various platforms.

Not Seeing Binaries For Your Platform?

Run currently uses goreleaser to generate release assets.

Feel free to open an issue to discuss additional target platforms, or even create a PR against the .goreleaser.yml configuration.

Brew

Brew Core

Run is now available on homebrew core:

install run via brew core

$ brew install run

Brew Tap

In addition to being available in brew core, I have also created a tap to ensure the latest version is always available:

install run directly from tap

$ brew install tekwizely/tap/run

install tap to track updates

$ brew tap tekwizely/tap

$ brew install run

AUR

For Archlinux users, a package is available on the AUR:

install run from AUR using yay

$ yay -S run-git

NPM / Yarn

NPM & Yarn users can install run via the @tekwizely/run package:

$ npm i '@tekwizely/run'

$ yarn add '@tekwizely/run'

Other Package Managers

I hope to have other packages available soon and will update the README as they become available.


Contributing

To contribute to Run, follow these steps:

  1. Fork this repository.
  2. Create a branch: git checkout -b <branch_name>.
  3. Make your changes and commit them: git commit -m '<commit_message>'
  4. Push to the original branch: git push origin <project_name>/<location>
  5. Create the pull request.

Alternatively see the GitHub documentation on creating a pull request.


Contact

If you want to contact me you can reach me at [email protected].


License

The tekwizely/run project is released under the MIT License. See LICENSE file.


Just Looking for Bash Arg Parsing?

If you happened to find this project on your quest for bash-specific arg parsing solutions, I found this fantastic S/O post with many great suggestions:


Contributors ✨

Thanks goes to these wonderful people (emoji key):


chabad360

πŸ“– πŸš‡ πŸ›

Dawid Dziurla

πŸš‡

Bob "Wombat" Hogg

πŸ“–

Gys

πŸ›

Robin Burchell

πŸ’»

This project follows the all-contributors specification. Contributions of any kind welcome!