#!/bin/zsh
# add some boilerplate, and customize the help message
SCRIPT_NAME=$0
PROJECT_NAME=toolshell
source ./toolshell.inc
# set up your project's commands and checkers
add_check "[[ -f .yarn/install-state.gz ]]" "yarn --immutable"
set_start_command "yarn start"
# call main to parse the cli arguments and perform the required actions
toolshell_main
Z-Shell based domain specific language for tooling, scripting, workflows etc.
Helps you write shell scripts that help users run common tasks in your project, without figuring out the tools, build systems, scripts, run commands etc involved.
Skip to:
-
Download the toolshell.inc file to your repo
-
Create a util.zsh file that imports the toolshell.inc file and invokes toolshell_main:
#!/bin/zsh SCRIPT_NAME=util.zsh PROJECT_NAME=mycoolapp source ./toolshell.inc # fill in later toolshell_main
-
Add commands, cleanup folders etc
#!/bin/zsh
SCRIPT_NAME=util.zsh
PROJECT_NAME=mycoolapp
source ./toolshell.inc
requires_tool node 16
set_start_command "yarn start"
set_build_command "yarn build"
toolshell_main
- Done
Download the toolshell inc file:
wget -o ./toolshell.inc https://raw.githubusercontent.com/boxmein/toolshell/master/build/toolshell.inc
Create some subcommands: see examples
cat <<EOF > util.zsh
#!/bin/zsh
SCRIPT_NAME=$0
PROJECT_NAME=mycoolapp
source ./toolshell.inc
# check that some commands are installed
requires_tool yarn 1.22.0
requires_tool node
# add command-aliases (e.g. ./x test)
add_command tests "yarn test"
add_command build "yarn build"
add_command publish "yarn publish"
add_command lint "yarn lint"
# set what ./x start does
set_start_command "yarn start"
# delete build/ when running ./x cleanup
add_cleanup_dir build
# parse cli flags, and perform actions
toolshell_main
EOF
Make it executable and add it to your repo:
chmod +x ./util.zsh
git add ./util.zsh
git commit -m "common utilities"
Python 3
#!/bin/zsh
SCRIPT_NAME=$0
PROJECT_NAME=toolshell
source ./toolshell.inc
requires_tool python3 3
requires_tool pip3
add_check "pip3 show mitmproxy"
set_start_command "python3 ./mitmfaultinjection.py"
toolshell_main
Rust
#!/bin/zsh
SCRIPT_NAME=$0
PROJECT_NAME=toolshell
source ./toolshell.inc
requires_tool cargo
set_build_command "cargo build"
add_command formatter "cargo fmt"
add_command linter "cargo clippy"
add_command check "cargo check"
set_start_command "./target/debug/myrustproject"
add_cleanup_dir target
toolshell_main
Kotlin + Gradle
#!/bin/zsh
SCRIPT_NAME=$0
PROJECT_NAME=toolshell
source ./toolshell.inc
set_build_command "./gradlew :build"
set_start_command "./gradlew :run"
add_command "gradle task list" "./gradlew :tasks"
add_command "tests" "./gradlew :check"
add_command "create jar" "./gradlew :jar "
add_command "create distribution zip" "./gradlew :distZip"
add_command "install" "./gradlew :installDist"
add_command "docker build" "./gradlew :docker"
add_command "upload docker image" "./gradlew :dockerPush"
add_command "javadoc" "./gradlew :javadoc"
# add more gradle tasks here if needed
add_cleanup_dir build .gradle
toolshell_main
These are current features of the shell script DSL:
- It's all zsh
set_build_command "<command>"
- command to run with./x build
set_start_command "<command>"
- command to run with./x start
add_cleanup_dir ./build
- removes./build
when you call./x clean
add_command "<cmdname>" "<commands...>"
- add a command alias (./x cmdname = zsh -c commands...
)requires_tool <command_name> [version_range]
- check if command_name is in PATHadd_check "<command_name>"
- must exit with status 0 before the rest of the tools workadd_initializer "<check_command>" "<fixer>"
- run check_command, if it fails, offer to run fixer (useful escape hatch for ensuring the project is ready)
It also includes a useful library:
- Installing tools from package managers
- Comparing file ages
- Matching semver ranges
- Synchronizing files to a remote server
Assuming that you make your build script into the file ./x
:
Since the script you write with ./x
is a regular zsh script, you have to
source the DSL commands into it.
Here's an empty build file to start with:
#!/bin/zsh
SCRIPT_NAME=$0
PROJECT_NAME=toolshell
source ./toolshell.inc
# commands go here...
toolshell_main
It's nicer to start from an example. Some are above, some are in the examples folder.
Runs registered commands depending on the CLI arguments.
Reference:
- Parses command line arguments
- Handles -v and --help
- When no arguments are passed:
- Starts fuzzy finder
fzf
with list of runnable commands. - When a command is selected, runs it.
- Starts fuzzy finder
- When 1 argument is passed, parses it as a command name and runs it.
- When
start
is the first argument, runs the command registered as the start script. - When
build
is the first argument, runs the command registered as the build script. - Lookups the script registered with that name and runs it.
- When
- When more than 1 arguments are passed:
- Looks up the script registered with that name and runs it.
Syntax:
toolshell_main
Registers a command to run when ./x start
is run.
Shows the "./x start" command in the help text.
Syntax:
set_start_command "<command>"
Example:
set_start_command "python3 ./mycommand.py"
Registers a command to run when ./x build
is run.
Shows the "./x build" command in the help text.
Syntax:
set_build_command "<command>"
Example:
set_build_command "./gradlew :build"
Registers folders to delete when ./x cleanup
is run.
Syntax:
add_cleanup_dir <path> <path...>
Example:
add_cleanup_dir target/ ./dist/ ./__cache__/
Registeres a custom script.
Custom scripts can be invoked:
- On the command line:
./x <cmd>
- With the fuzzy finder:
./x
, select<cmd>
, press the Enter key.
Custom scripts appear in the help as:
additional scripts:
./util.zsh run build: alias for "yarn build"
Syntax:
add_command "<cmd>" "<command>"
Example:
add_command "tests" "cargo test"
add_command "dependency updates" "yarn upgrade-interactive"
requires_tool "<tool>" "<version>"
Checks that a given command line utility is available in the PATH
.
If not, shows an error message and exits the script.
If <version>
is provided, runs <commandname> --version
and matches the output to the version
string. If the output of <commandname> --version
does not contain the version
string, shows an error message and exits the script.
For example:
requires_tool python3 3.10
# invokes `python3 --version | grep 3.10`
# if matches: OK
# if not matches: error, exit
If "--offer-install" is set, and the tool is missing, will offer to install the tool using the system package manager by running a command like apt-get install <cmd>
.
Syntax:
requires_tool [--offer-install] <commandname> <version_check>
Example:
requires_tool yarn 1.22
requires_tool --offer-install make
Runs a command to validate that something is OK before continuing with the tooling script.
This is useful to check that the user's environment is set up correctly, or that the project is in a clean state.
If the command returns a nonzero status code, shows an error message and exits.
If the command returns a zero status code, silently continues.
Syntax:
add_check "<command>"
Example:
# Check that docker is up.
add_check "docker ps"
Runs a command to detect if the project is ready to run. If the command exits with a nonzero status code, it will invoke the second command to correct the problem.
Syntax:
add_initializer "<command>" "<command>"
Example:
add_initializer "_toolshell_is_newer_than package.json node_modules/.yarn-integrity" \
"yarn install --immutable"
toolshell
includes a number of helper functions for use in your tooling.
Checks if file A is newer than file B. Returns zero status code if file A is newer.
Returns status code 1 if file B does not exist, or file B is older than file A.
Syntax:
_toolshell_is_newer_than "file_a" "file_b`
Example:
if _toolshell_is_newer_than package.json node_modules/.yarn-integrity; then
yarn install --immutable
fi
Uses semver rules to determine if a version matches a specified version range.
Returns status code 0 if matches, status code 1 if does not match.
Supported version ranges:
- Caret syntax
^1.2.0
- pass if minor or patch version is greater than or equal to desired - Tilde syntax
~1.2.0
- pass if patch version is greater than or equal to desired - Greater-than syntax
>=1.2.0
- pass if all versions are greater-than or equal to desired
Syntax:
_toolshell_range_matches_version "<desired_range>" "<actual_version>"
Example:
_toolshell_range_matches_version "^1.0.0" "1.2.5" # ok
_toolshell_range_matches_version ">=1.0.0" "2.8.04" # ok
_toolshell_range_matches_version "~12.3.2200" "1.2.3" # fail
toolshell
Currently: benevolent dictator for life
Standard GitHub flow: fork, pull-request, validate, merge
MIT license
- Specializations
- Specialized sourceable Zsh scripts that help with certain project environments
- Yarn, NPM, Node, Lerna, Yarn Workspaces, etc
- Gradle
- Bazel
- Pip, Poetry, Pipenv
- Docker
- Dependency graphs, caching, etc -- many other tools do it better. Invoke those tools with this.
- Downloading tools -- Package managers etc vary a lot. You might have
vendoring or multiple SDKs installed. You can use
tool TOOLNAME VERSION
to check for existence. - Installable CLI - toolshell should be vendored into the repo.
- Package manager packages - toolshell should be vendored into the repo.