| 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.9.0 — New ops-init-extensions-py command to initialize a Python-based extensions repo — extension logic in Python, repo management in bash, with a virtual environment and console_scripts entry points. ops-init-extensions now prompts for a function prefix. 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.
Use the built-in init command to scaffold a pure-bash extensions repo:
export OPSCLI_EXTENSIONS_PATH="$HOME/repos/my-functions"
ops-init-extensionsThe command prompts for a function prefix (e.g. mycompany) and generates a repo with a demo function and git history. Any .sh file in any subfolder is sourced automatically on ops-reload.
For extensions that benefit from Python (API clients, structured data, cloud SDKs), use the Python-aware init:
export OPSCLI_EXTENSIONS_PATH="$HOME/repos/my-functions"
ops-init-extensions-pyThis generates a repo with:
- Python extensions — argparse-based CLIs installed as console_scripts entry points in a
.venv - Bash wrappers — thin
.shfiles with cheat blocks soops-functionsdiscovers them - Bash management functions — info, dev/prod switching, and update (including automatic
pip install -e .after version updates)
See the generated repo's README for details on adding new Python extensions.
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.
| 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 bash extensions repo at $OPSCLI_EXTENSIONS_PATH |
ops-init-extensions-py |
Initialize a new Python-based 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