Simple CLI task runner
Lets takes the best from Makefile and bash and presents you a simple yet powerful tool for defining and running cli tasks and commands.
Just describe your commands in lets.yaml
and lets will do the rest.
Very alpha software. Things may and will change/break
Shell script:
This will install lets
binary to /usr/local/bin
directory. But you can change install location to any directory you want
sudo curl -sfL https://raw.githubusercontent.com/lets-cli/lets/master/install.sh | sudo sh -s -- -b /usr/local/bin
Binary (Cross-platform):
Download the version you need for your platform from Lets Releases.
Once downloaded, the binary can be run from anywhere.
Ideally, you should install it somewhere in your PATH for easy use. /usr/local/bin
is the most probable location.
Arch Linux:
You can get binary release from https://aur.archlinux.org/packages/lets-bin/
If you are using yay
as AUR helper:
yay -S lets-bin
Also you can get bleeding edge version from https://aur.archlinux.org/packages/lets-git/
yay -S lets-git
- Create
lets.yaml
file in your project directory - Add commands to
lets.yaml
config. Config reference
commands:
echo:
description: Echo text
cmd: |
echo "Hello"
run:
description: Run some command
options: |
Usage: lets run [--debug] [--level=<level>]
Options:
--debug, -d Run with debug
--level=<level> Log level
cmd: |
env
- Run commands
lets echo
# will print Hello
lets run --debug --level=info
# will print
# LETSOPT_DEBUG=true
# LETSOPT_LEVEL=info#
# LETSCLI_DEBUG=--debug
# LETSCLI_LEVEL=--level info
At first run lets
will create .lets
directory in the current directory.
lets
uses .lets
to store some specific data such as checksums, etc.
It's better to add .lets
to your .gitignore
end exclude it in your favorite ide.
Config schema
key: version
type: semver string
Specify minimum required lets
version to run this config.
Example:
version: '0.0.20'
key: shell
type: string
required: true
Specify shell to use when running commands
Example:
shell: bash
key: env
type: string
Specify global env for all commands.
Example:
shell: bash
env:
MY_GLOBAL_ENV: "123"
key: env
type: string
Specify global eval_env for all commands.
Example:
shell: bash
eval_env:
CURRENT_UID: echo "`id -u`:`id -g`"
key: mixins
type: list of string
Allows to split lets.yaml
into mixins (mixin config files).
To make lets.yaml
small and readable its convenient to split main config into many smaller ones and include them
Example:
# in lets.yaml
...
shell: bash
mixins:
- test.yaml
commands:
echo:
cmd: echo Hi
# in test.yaml
...
commands:
test:
cmd: echo Testing...
And lets test
works fine.
key: commands
type: mapping
required: true
Mapping of all available commands
Example:
commands:
test:
description: Test something
key: description
type: string
Short description of command - shown in help message
Example:
commands:
test:
description: Test something
key: cmd
type: string or array of strings
Actual command to run in shell.
Can be either a string (also a multiline string) or an array of strings.
The main difference is that when specifying an array, all args, specified in terminal will be appended to cmd array
Example single string:
commands:
test:
description: Test something
cmd: go test ./... -v
Example multiline string:
commands:
test:
description: Test something
cmd: |
echo "Running go tests..."
go test ./... -v
Example array of strings:
commands:
test:
description: Test something
cmd:
- go
- test
- ./...
When run with cmd as array of strings:
lets test -v
the -v
will be appended, so the resulting command to run will be go test ./... -v
key: depends
type: array of string
Specify what commands to run before the actual command. May be useful, when have one shared command.
For example, lets say you have command build
, which builds docker image.
Example:
commands:
build:
description: Build docker image
cmd: docker build -t myimg . -f Dockerfile
test:
description: Test something
depends: [build]
cmd: go test ./... -v
fmt:
description: Format the code
depends: [build]
cmd: go fmt
build
command will be executed each time you run lets test
or lets fmt
key: options
type: string (multiline string)
One of the most cool things about lets
than it has built in docopt parsing.
All you need is to write a valid docopt for a command and lets will parse and inject all values for you.
More info http://docopt.org
When parsed, lets
will provide two kind of env variabled:
LETSOPT_<VAR>
LETSCLI_<VAR>
How does it work?
Lets see an example:
commands:
echo-env:
description: Echo env vars
options:
Usage: lets [--log-level=<level>] [--debug] <args>...
Options:
<args>... List of required positional args
--log-level,-l Log level
--debug,-d Run with debug
cmd: |
echo ${LETSOPT_ARGS}
app ${LETSCLI_DEBUG}
So here we have:
args
- is a list of required positional args
--log-level
- is a key-value flag, must be provided with some value
--debug
- is a bool flag, if specified, means true, if no specified means false
In the env of cmd
command there will be available two types of env variables:
lets echo-env --log-level=info --debug one two three
Parsed and formatted key values
echo LETSOPT_ARGS=${LETSOPT_ARGS} # LETSOPT_ARGS=one two three
echo LETSOPT_LOG_LEVEL=${LETSOPT_LOG_LEVEL} # LETSOPT_LOG_LEVEL=info
echo LETSOPT_DEBUG=${LETSOPT_DEBUG} # LETSOPT_DEBUG=true
Raw flags (useful if for example you wan to pass --debug
as is)
echo LETSCLI_ARGS=${LETSCLI_ARGS} # LETSCLI_ARGS=one two three
echo LETSCLI_LOG_LEVEL=${LETSCLI_LOG_LEVEL} # LETSCLI_LOG_LEVEL=--log-level info
echo LETSCLI_DEBUG=${LETSCLI_DEBUG} # LETSCLI_DEBUG=--debug
key: env
type: mapping string => string
Env is as simple as it sounds. Define additional env for a commmand:
Example:
commands:
test:
description: Test something
env:
GO111MODULE: "on"
GOPROXY: https://goproxy.io
cmd: go build -o lets *.go
key: eval_env
type: mapping string => string
Same as env but allows you to dynamically compute env:
Example:
commands:
test:
description: Test something
eval_env:
CURRENT_UID: echo "`id -u`:`id -g`"
CURRENT_USER_NAME: echo "`id -un`"
cmd: go build -o lets *.go
Value will be executed in shell and result will be saved in env.
key: checksum
type: array of string | mapping string => array of string
Checksum used for computing file hashed. It is useful when you depend on some files content changes.
In checksum
you can specify:
- a list of file names
- a list of file regext patterns (parsed via go
path/filepath.Glob
)
or
- a mapping where key is name of env variable and value is:
- a list of file names
- a list of file regext patterns (parsed via go
path/filepath.Glob
)
Each time a command runs, lets
will calculate the checksum of all files specified in checksum
.
Result then can be accessed via LETS_CHECKSUM
env variable.
If checksum is a mapping, e.g:
commands:
build:
checksum:
deps:
- package.json
doc:
- Readme.md
Resulting env will be:
LETS_CHECKSUM_DEPS
- checksum of deps filesLETS_CHECKSUM_DOC
- checksum of doc filesLETS_CHECKSUM
- checksum of all checksums (deps and doc)
Checksum is calculated with sha1
.
If you specify patterns, lets
will try to find all matches and will calculate checksum of that files.
Example:
shell: bash
commands:
app-build:
checksum:
- requirements-*.txt
cmd: |
docker pull myrepo/app:${LETS_CHECKSUM}
docker run --rm myrepo/app${LETS_CHECKSUM} python -m app
key: persist_checksum
type: bool
This feature is useful when you want to know that something has changed between two executions of a command.
persist_checksum
can be used only if checksum
declared for command.
If set to true, each run all calculated checksums will be stored to disk.
After each subsequent run lets
will check if new checksum and stored checksum are different.
Result of that check will be exposed via LETS_CHECKSUM_CHANGED
and LETS_CHECKSUM_[checksum-name]_CHANGED
env variables.
IMPORTANT: New checksum will override old checksum only if cmd has exit code 0
Example:
commands:
build:
persist_checksum: true
checksum:
deps:
- package.json
doc:
- Readme.md
Resulting env will be:
-
LETS_CHECKSUM_DEPS
- checksum of deps files -
LETS_CHECKSUM_DOC
- checksum of doc files -
LETS_CHECKSUM
- checksum of all checksums (deps and doc) -
LETS_CHECKSUM_DEPS_CHANGED
- is checksum of deps files changed -
LETS_CHECKSUM_DOC_CHANGED
- is checksum of doc files changed -
LETS_CHECKSUM_CHANGED
- is checksum of all checksums (deps and doc) changed
lets
has builtin environ variables.
LETS_DEBUG
- enable debug messagesLETS_CONFIG
- changes defaultlets.yaml
file path (e.g. LETS_CONFIG=lets.my.yaml)LETS_CONFIG_DIR
- changes path to dir wherelets.yaml
file placedLETS_NO_COLOR_OUTPUT
- disables colored output
To build a binary:
go build -o lets *.go
To install in system
go build -o lets *.go && sudo mv ./lets /usr/local/bin/lets
Or if you already have lets
installed in your system:
lets build-and-install
After install - check version of lets - lets --version
- it should be development
It will install lets
to /usr/local/bin/lets and set version to development with current tag and timestamp
To run all tests:
lets test
To run unit tests:
lets test-unit
To get coverage:
lets coverage
To test lets
output we using bats
- bash automated testing:
lets test-bats
# or run one test
lets test-bats global_env.bats
To release a new version:
lets release 0.0.<n> -m "implement some new feature"
This will create annotated tag with 0.0. and run git push --tags
lets
releases must be backward compatible. That means every new lets
release must work with old configs.
For situations like e.g. new functionality, there is a version
in lets.yaml
which specifies minimum required lets
version.
If lets
version installed on user machine is less than the one specified in config it will show and error and ask user to upgrade lets
version.
You can use Bash/Zsh/Oh-My-Zsh completion in you terminal
-
Bash
Add
source <(lets completion -s bash)
to your~/.bashrc
or~/.bash_profile
-
Zsh/Oh-My-Zsh
There is a repo with zsh plugin with instructions