| repository | opscli |
| version | CHANGELOG.md |
| owner | brtlvrs |
| license | MIT |
A BASH shell framework that is sourced into an interactive shell (via .bashrc) or into scripts. It provides a structured, reloadable function library with built-in logging, cheatsheet generation, and version management.
Fork this repository if you want to contribute to the framework. To add your own functions, create a separate extensions repo — see Extensions.
v2.7.3 — Colour refinements for write* functions: writeERR is now red, writeFAIL is black on red, writeOK is black on green. Level labels in output are now always uppercase. Demo function renamed to writeDEMO. See the full CHANGELOG.
- How it works
- Prerequisites
- Installation
- Extensions
- Key aliases
- Using the library in scripts
- Console logging
- Writing functions
- Debugging
- Contributing to the framework
opscli uses a two-repo model:
| Repo | Purpose |
|---|---|
| opscli (this repo) | The framework — foundational functions, logging, aliases, version management. Updated via ops-update. Never edit directly. |
| your extensions repo | Your custom functions. A separate git repo you own and version independently. |
At shell startup, .bashrc sources the framework. If OPSCLI_EXTENSIONS_PATH points to your extensions repo, the framework automatically sources it too — so ops-reload reloads everything in one shot.
.bashrc
└── source opscli/library.sh
├── loads framework functions
└── sources $OPSCLI_EXTENSIONS_PATH (if set)
bashgitjq(optional)yq(optional)
-
Clone this repo under
$HOME/repos/:git clone <opscli-url> $HOME/repos/opscli
-
Add the following to your
~/.bashrc:# point to your extensions repo (optional but recommended) export OPSCLI_EXTENSIONS_PATH="$HOME/repos/my-functions" # load the opscli framework source $HOME/repos/opscli/library.sh
-
Reload your shell:
source ~/.bashrc
-
Update to the latest tagged version:
ops-update
Your custom functions live in a separate repo that you create and manage. The framework sources it automatically when OPSCLI_EXTENSIONS_PATH is set.
mkdir -p $HOME/repos/my-functions
cd $HOME/repos/my-functions
git initAdd subfolders for your functions — any .sh file in any subfolder is sourced automatically:
my-functions/
├── kubernetes/
│ └── helpers.sh
├── aws/
│ └── helpers.sh
└── daily/
└── shortcuts.sh
Set OPSCLI_EXTENSIONS_PATH in your .bashrc (before the source line) and reload. Your functions are now available alongside the framework functions.
ops-update # updates the framework, leaves your extensions untouched
ops-reload # reloads both framework and extensionsBecause your extensions live in a separate repo, ops-update (which does a git reset --hard inside the framework repo) never touches your files.
Follow the same conventions as framework functions — see Writing functions. Use the ops::* namespace so your functions appear in ops-functions and are cleaned up correctly on ops-reload.
| Alias | Description |
|---|---|
ops-reload |
Reload the framework and extensions from their respective paths |
ops-functions |
Browse the full function cheatsheet (piped through less -R) |
ops-alias |
Show alias summary only |
ops-info [key] |
Show library metadata (path, version, git url, env, …) |
ops-update [--beta] [tag] |
Fetch tags and reset framework to a version; --beta targets the latest beta release |
ops-init-extensions |
Initialize a new extensions repo at $OPSCLI_EXTENSIONS_PATH |
shellTMPdir |
Create a hidden temp directory under $HOME |
shellTMP |
Create a temp file inside a shellTMPdir directory |
ops-info accepts a key argument to return a single value:
ops-info version # current version tag or branch
ops-info git_url # remote origin URL
ops-info prod_path # path to the framework clone
ops-info env # "prod" or "dev"
ops-info --all # print all of the aboveWhen the library is sourced, library.sh exports $OPSCLI_PATH. Scripts can use this to reload the library and enforce a minimum version:
#!/bin/bash
[[ ! -d ${OPSCLI_PATH} ]] && { echo "WARNING: opscli not loaded."; exit 1; }
unset OPSCLI_LOADED
source ${OPSCLI_PATH}/library.sh
ops::version::isSupported v2.0.0 || exit 1
# ... rest of the scriptIf OPSCLI_EXTENSIONS_PATH is set in the environment, extensions are loaded automatically here too.
Pass -v <version> directly to library.sh to combine the source and version check:
source ${OPSCLI_PATH}/library.sh -v v2.0.0 || exit 1Use these functions instead of raw echo. All output goes to stderr. Every function produces a compact single-line format: a coloured timestamp + label, a symbol, then the message. Run writeDEMO to see them all in one go.
| Function | Symbol | Colour | Notes |
|---|---|---|---|
writeINF |
→ |
cyan | General informational message |
writeOK |
✓ |
black on green | Pass / success result |
writeFAIL |
✗ |
black on red | Fail / validation result |
writeNOTE |
• |
grey | Subtle annotation or context |
writeWRN |
▲ |
yellow | Warning; call location printed on last line |
writeERR |
✖ |
red | Hard error; call location printed on last line |
writeTODO |
☐ |
yellow | Marks incomplete code; call location printed on last line |
writeDBG |
⚙ |
grey | Debug; call location on last line; only printed when $DEBUG or $debug is set |
writeINF "Library loaded successfully."
writeOK "Connection established."
writeFAIL "Health check failed."
writeNOTE "Skipping optional step."
writeWRN "Config value missing, using default."
writeERR "Fatal: could not connect."
writeTODO "Implement retry logic."
DEBUG=true writeDBG "Variable value: $myvar"Use templates/function.tmpl as a starting point. Every function must include a cheat block so it appears in ops-functions and ops-alias:
#-- START CHEAT --
# Function: ops::namespace::functionname
# Alias: ops-myalias
# Description: One-line description
# Parameters:
# -h | --help Show help
# $1 Some positional argument
#-- END CHEAT --Standard function structure:
function ops::namespace::name() {
function ops::namespace::name::_usage() { cat <<-EOF
usage: ops-myalias [-h] <arg>
EOF
}
function ops::namespace::name::_guardrails() { … }
function ops::namespace::name::_process-arguments() {
local arguments=($(ops::common::splitArgs "$@"))
for (( i=0; i<${#arguments[@]}; i++ )); do
case ${arguments[i]} in
-h|--help) ops::namespace::name::_usage; return 1 ;;
esac
done
}
function ops::namespace::name::_main() { … }
ops::namespace::name::_guardrails "$@" || return $?
ops::namespace::name::_process-arguments "$@" || return $?
ops::namespace::name::_main || return $?
}
alias ops-myalias='ops::namespace::name'ops::common::splitArgs normalises --key=value into --key value pairs before the case loop.
Place the .sh file in any subfolder of your extensions repo; it is sourced automatically on the next ops-reload.
set -x is safe to use interactively. ops::common::appendPromptCommand prepends set +x to PROMPT_COMMAND, so xtrace is silenced automatically before the next prompt — stray set -x calls will not pollute your interactive shell.
Enable writeDBG output:
export DEBUG=true # or: debug=trueWhen DEBUG is set, temp directories created by shellTMPdir are not cleaned up on exit or CTRL-C, making it easier to inspect intermediate state.
To contribute changes to the framework itself (not extensions), you need both the production and development clones.
ops-init-dev # clones the framework repo to $HOME/repos/opscli.dev and creates the dev branch
ops-dev # switch the active library to the dev clone| Alias | Description |
|---|---|
ops-dev |
Reload from the development clone (opscli.dev) |
ops-prod |
Reload from the production clone (opscli) |
ops-update automatically switches from dev to prod when invoked from the dev clone, so you do not need to run ops-prod first.
git merge main && git push— sync the dev branch with the latest release- Add or modify
.shfiles in the dev clone ops-reload— pick up the changes in the current shellgit committhe changes- When ready to release, follow the release process in CLAUDE.md