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

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Run: Easily manage and invoke small scripts and wrappers

GitHub repo size All Contributors GitHub stars GitHub forks

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
Registering Commands

When registering commands, run treats the command name as case-insensitive and subject to command override rules.

case-insensitive override example

For example, run will generate an error if a command name is defined multiple times in the same runfile, even if the names use different cases:

Runfile

hello-world:
  echo "Hello, world"

HELLO-WORLD:
  echo "HELLO, WORLD"

list commands

$ run list

run: Runfile: command hello-world defined multiple times in the same file: lines 1 and 4
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 displays command names as they are originally defined:

list commands

$ run list

Commands:
  ...
  Hello-World
  ...

show help for Hello-World command

$ run help hello-world

Hello-World: no help available.

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

Making Options Required

You can use ! to indicate that an option is required:

# OPTION NAME! -n,--name <name> Name to say hello to
Required Indicator on Help Text

Required options will be indicated in help text:

  -n, --name <name> (required)
        Name to say hello to
Error When Required Option Not Provided

An error will be generated if a required option is not provided:

hello: ERROR: Missing required option:
  -n, --name <name>
        Name to say hello to

Explicitly Marking Options as "Optional"

Although options are already optional by default, you can use ? to explicitly indicate that an option is optional:

# OPTION NAME? -n,--name <name> Name to say hello to

NOTE: This exists mostly for parity with ! and behaves the same as when it is not used

Providing A Default Option Value

You can use ?= to specify a default value for an option, which will be used if the option is not provided:

# OPTION NAME ?= Newman -n,--name <name> Name to say hello to

output

$ run hello

Hello, Newman

Note: Any standard variable assignment value can be used (quoted strings, variable references, etc)

Default Indicator on Help Text

Default values will be indicated in help text:

  -n, --name <name> (default: 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
Boolean Default Option Values

You can specify a default value for boolean options, but they behave slightly different from standard options:

# OPTION NEWMAN ?= enabled --newman Say hello to Newman
Defaulted Value Always Assumed to be True

The content of the default value text is not used to determine the option's default true/false value.

Why?

Since boolean values are already always false by default, providing a "default value" can only have the effect of defaulting the value to true.

output

$ run hello

Hello, Newman
Default Indicator on Help Text

Even though a boolean option with provided default is always assumed to default to true, the default value text is still useful in that it will be displayed in the help text:

  --newman (default: enabled)

This allows you to give better messaging than just "true" or "1" (i.e "enabled" in this example)

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
$ run help               # Default value = true if option has ?=

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 if option does not have ?=

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, then run will walk up the folder hierarchy, checking each folder for the currently-configured 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 the Runfile 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

Runfile Attributes

Attributes are special variables used by the Run engine.

Their names start with . to avoid colliding with runfile variables and environment variables.

Following is the list of Run's attributes:

Attribute Description
.SHELL Contains the shell command that will be used to execute command scripts. See Script Shells for more details.
.RUN Contains the absolute path of the run binary currently in use. Useful for Invoking Other Commands & Runfiles.
.RUNFILE Contains the absolute path of the primary Runfile.
.RUNFILE.DIR Contains the absolute path of the parent folder of the primary runfile.
.SELF Contains the absolute path of the current (primary or included) runfile.
.SELF.DIR Contains the absolute path of the parent folder of the current runfile.

Exporting Attributes

In order to access an attribute's value within your commands, you'll need to assign them to an exported variable.

Older versions of Run required you to use a variable assignment:

Runfile

EXPORT RUNFILE     := ${.RUNFILE}
EXPORT RUNFILE_DIR := ${.RUNFILE.DIR}

## Prints the value of .RUNFILE
runfile:
    echo "${RUNFILE}"

## Prints the value of .RUNFILE.DIR
runfile-dir:
    echo "${RUNFILE_DIR}"

Newer versions of Run now support less verbose options:

Simple Export

You can quickly export an attribute with a default variable name:

Runfile

EXPORT .RUNFILE, .RUNFILE.DIR

## Prints the value of .RUNFILE
runfile:
    echo "${RUNFILE}"

## Prints the value of .RUNFILE.DIR
runfile-dir:
    echo "${RUNFILE_DIR}"

With this technique, Run uses the attribute's name to determine the exported variable's name by:

  • Removing the leading . character
  • Substituting any remaining . characters with _
Export With Name

If you want to export an attribute with a non-default variable name, you can use the AS syntax:

EXPORT .RUNFILE     AS RF
EXPORT .RUNFILE.DIR AS RFD

## Prints the value of .RUNFILE
runfile:
    echo "${RF}"

## Prints the value of .RUNFILE.DIR
runfile-dir:
    echo "${RFD}"

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: Runfile:7: Variable HELLO not defined

$ run name

run: ERROR: Runfile:7: Variable HELLO not defined

example with HELLO

$ HELLO=Hello run newman

Hello, Newman

$ HELLO=Hello run name

run: ERROR: Runfile:16: 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.


Includes - Runfiles

Includes let you organize and configure commands across multiple Runfiles.

You can include other Runfiles using the following syntax:

INCLUDE <file pattern> | "<file pattern>" | '<file pattern>'

Simple example:

file layout

Runfile
Runfile-hello

Runfile

INCLUDE Runfile-hello

Runfile-hello

hello:
    echo "Hello from Runfile-hello"

output

$ run hello

Hello from Runfile-hello

File Globbing

Run utilizes goreleaser/fileglob in order support file globbing for includes.

According to their README, fileglob supports:

  • Asterisk wildcards (*)
  • Super-asterisk wildcards (**)
  • Single symbol wildcards (?)
  • Character list matchers with negation and ranges ([abc], [!abc], [a-c])
  • Alternative matchers ({a,b})
  • Nested globbing ({a,[bc]})
  • Escapable wildcards (\{a\}/\*)

Fileglob Example:

file layout

Runfile
1/1/Runfile-1
2/2/Runfile-2
3/3/Runfile-3

Runfile

INCLUDE **/Runfile-*

Working Directory

Include names / glob-patterns are resolved relative to the Primary runfile's containing directory.

File(s) Not Found

Default: OK For Glob

When using a globbing pattern, Run considers it OK if the pattern results in no files being found.

This makes it possible to support features like an optional Runfile include directory, or the ability to start a project folder with no includes but have them automatically picked up as you add them.

Runfile

INCLUDE maybe_some_runfiles/Runfile-*  # OK if no files found
Force Error If No Files Found

To force an error if no files are found when using a globbing pattern, use ! :

Runfile

INCLUDE ! maybe_some_runfiles/Runfile-*  # ERROR if no files found
Default: BAD For Single File

When using a single filename (no globbing), Run considers it an error if the include file is not found.

Runfile

INCLUDE Runfile-must-exist  # Errors if file not found

output

$ run list

run: include runfile not found: 'Runfile-must-exist'
Skip Error If File Not Found

To skip generating an error if no file is found when using a single filename, use ? :

Runfile

INCLUDE ? Runfile-might-exist  # OK if file not found

Avoiding Include Loops

Run keeps track of already-included runfiles and will silently avoid including the same runfile multiple times.

Runfile

INCLUDE Runfile-hello
INCLUDE Runfile-hello  # Silently skipped

Overriding Commands

Run allows you override commands, as long as they were originally registered in a different Runfile.

Runfile

## defined in Runfile
command1:
  echo command1 from Runfile

INCLUDE Runfile-include

## defined in Runfile
command2:
  echo command2 from Runfile

Runfile-include

## defined in Runfile-include
command1:
  echo command1 from Runfile-include

## defined in Runfile-include
command2:
  echo command2 from Runfile-include

list commands

$ run list

Commands:
  ...
  command1    defined in Runfile-include
  command2    defined in Runfile

Notice that the included runfile overrides command1, but the primary runfile overrides command2.

Cannot Re-Register Command In Same Runfile

Run will error when attempting to register a command multiple times within the same Runfile:

Runfile

hello-world:
  echo "Hello, world"

hello-world:
  echo "Hello, world"

list commands

$ run list

run: Runfile: command hello-world defined multiple times in the same file: lines 1 and 4
Overrides Are Case-Insensitive

Run's override matching is case-insensitive:

Runfile

## defined in Runfile
command1:
  echo command1 from Runfile

include Runfile-include

Runfile-include

## defined in Runfile-include
COMMAND1:
  echo command1 from Runfile-include

list commands

$ run list

Commands:
  ...
  command1    defined in Runfile-include

Notice that COMMAND1 from the included runfile overrides command1 from the primary runfile.

First Registered Command Defines Case For Help

Run keeps track of the original case used when a command is first registered, and uses it when displaying help:

Runfile

## defined in Runfile
COMMAND1:
  echo command1 from Runfile

include Runfile-include

Runfile-include

## defined in Runfile-include
command1:
  echo command1 from Runfile-include

list commands

$ run list

Commands:
  ...
  COMMAND1    defined in Runfile-include

Notice that the displayed name comes from the original registration in the primary runfile.

First Registered Command Defines Default Documentation

Run keeps track of the title & description when a command is first registered, and uses it if an overriding command does not define its own documentation:

Runfile

## title defined in Runfile
command1:
  echo command1 from Runfile

include Runfile-include

Runfile-include

command1:
  echo command1 from Runfile-include

list commands

$ run list

Commands:
  ...
  commmand1    title defined in Runfile

command output

$ run command1

command1 from Runfile-include

Notice that, even though command1 from the included runfile was invoked, the displayed title comes from the original registration in the primary runfile.

Commands Are Listed In The Order They Are Registered

Run keeps track of the order in which commands are registered, and maintains that order even if a command is later overridden:

Runfile

## defined in Runfile
command1:
  echo command1 from Runfile

## defined in Runfile
command2:
  echo command2 from Runfile

## defined in Runfile
command3:
  echo command3 from Runfile

include Runfile-include

Runfile-include

## defined in Runfile-include
command2:
  echo command2 from Runfile-include

list commands

$ run list

Commands:
  ...
  command1    defined in Runfile
  command2    defined in Runfile-include
  command3    defined in Runfile

Notice that command2 is still shown between command1 and command3, matching the order in which it was originally registered.


Includes - .ENV

.env files allow users to manage runfile configuration without modifying the Runfile directly.

Your Runfile can include .env files using the following syntax:

INCLUDE.ENV <file pattern> | "<file pattern>" | '<file pattern>'

Simple example:

Runfile.env

HELLO=Newman

Runfile

INCLUDE.ENV Runfile.env

##
# export HELLO
hello:
    echo "Hello, ${HELLO:-World}"

output

$ run hello

Hello, Newman

Notes:

  • Variables are immediately available, as if they had been defined in the same place in the Runfile.
  • Variables are not automatically exported.
  • Run uses the subosito/gotenv library to parse command output
  • # comments are supported and will be safely ignored
  • export keyword is optional and is (currently) ignored - This may be addressed in a future release
  • Simple variable references in assignments are supported, but variables defined within your Runfile are not (currently) accessible - This may be addressed in a future release
  • Visit the gotenv project page to learn more about which .env features are supported

File(s) Not Found

By default, Run considers it OK no .env file is found (using either a single filename or a globbing pattern).

To force an error if no file(s) are found, use !:

Runfile

INCLUDE.ENV ! Runfile-might-not-exist.env # ERROR if no file(s) found

Invoking Other Commands & Runfiles

RUN / RUN.AFTER / RUN.ENV Actions

You can invoke other commands (with arguments) from your Runfile before or after your command executes:

Runfile

##
# RUN hello "Newman"
# RUN.AFTER goodbye
test:
    echo "How are you?"

hello:
    echo "Hello, ${1:-World}"

goodbye:
    echo "Goodbye, now"

output

$ run test

Hello, Newman
How are you?
Goodbye, now

Note: Any standard variable assignment value can be used (quoted strings, variable references, etc)

Exported Variables

Your command's exported environment variables are also exported to the invoked command:

exported variable example

##
# EXPORT NAME := "Newman"
# RUN hello
test:
    echo "Goodbye, now"

hello:
  echo "Hello, ${NAME:-world}"

output

$ run test

Hello, Newman
Goodbye, now

Notes:

  • RUN.BEFORE is also supported, and behaves just like RUN
  • Commands are invoked in the order they are defined
  • Your command only runs if all previous RUN commands return exit code zero (0)
  • After commands only run if your command returns exit code zero (0)
  • Execution halts if any RUN returns a non-zero exit code
  • You cannot invoke builtin commands (help, version, etc)

Setting Variables via RUN.ENV

A common occurrence in Runfiles is to have a central command which computes a set of variables, which is then invoked by multiple other commands that need to use those variables:

eval example

##
# export .RUN, .RUNFILE
test:
    eval $( "$RUN" newman )
    echo "Hello, ${HELLO:-World}"

## Generates script suitable for 'eval' by caller
newman:
    echo "HELLO=Newman"

This technique works well, but Run also supports a similar feature using RUN.ENV:

run.env example

##
# RUN.ENV newman
# ASSERT [ -n "${HELLO}" ] "HELLO not defined"
test:
    echo "Hello, ${HELLO:-World}"

## Generates output compatible with simplified .env assignments
newman:
    echo "# Let's say hi to Newman"
    echo "export HELLO=Newman"

output

$ run newman

# Let's say hi to Newman
export HELLO=Newman
$ run test

Hello, Newman

Notes:

  • RUN.ENV commands are run after EXPORTS
  • RUN.ENV commands are run before ASSERTS
  • Commands invoked via RUN.ENV are expected to generate relatively simple variable assignments
  • Run uses the subosito/gotenv library to parse command output
  • # comments are supported and will be safely ignored
  • export keyword is optional and will be safely ignored
  • Simple variable references in assignments are supported, but variables defined within your Runfile are not (currently) accessible - This may be addressed in a future release
  • Visit the gotenv project page to learn more about which .env features are supported

.RUN / .RUNFILE Attributes

If you need more control while invoking other commands, Run makes it possible to invoke commands, or even other Runfiles, from within your command script.

Run exposes the following attributes:

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

NOTE: Even from inside an included Runfile, .RUNFILE will always reference the primary Runfile

Your command script can use these to invoke other commands:

Runfile

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

hello:
    echo "Hello, World"

output

$ run test

Hello, World

Hidden / Private Commands

Hidden Commands

You can mark a command as Hidden using a leading .:

hidden command example

##
# Prints 'Hello, Newman', then 'Goodbye, now'
# RUN hello Newman
test:
    echo "Goodbye, now"

## Hello command is hidden
.hello:
    echo "Hello, ${1:-world}"

Hidden commands don't show up when listing commands:

list commands

$ run list

Commands:
  ...
  test       Prints 'Hello, Newman', then 'Goodbye, now'

But they can still be invoked by using their full name, with .:

run hidden command

$ run .hello

Hello, world

Private Commands

You can mark a command as Private using a leading !:

private command example

##
# Prints 'Hello, Newman', then 'Goodbye, now'
# RUN hello Newman
test:
    echo "Goodbye, now"

## Hello command is private
!hello:
    echo "Hello, ${1:-world}"

Private commands don't show up when listing commands:

list commands

$ run list

Commands:
  ...
  test       Prints 'Hello, Newman', then 'Goodbye, now'

And they cannot be invoked from outside the Runfile:

try to run private command

$ run hello

run: command not found: hello

$ run '!hello'

run: command not found: !hello


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 autoconfigure 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

NIX

For Nix users, a package is available on nixpkgs:

Supported Platforms:

  • x86_64-darwin
  • aarch64-darwin
  • aarch64-linux
  • i686-linux
  • x86_64-linux

install run on NixOS

$ nix-env -iA nixos.run

install run on non-NixOs

$ nix-env -iA nixpkgs.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!