| 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.4.0 — Extensions support: set OPSCLI_EXTENSIONS_PATH to a separate repo and the framework sources it automatically on load and reload. Use ops-init-extensions to bootstrap a new extensions repo with a demo function. 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 [tag] |
Fetch tags and reset framework to a version |
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.
| Function | Colour | Notes |
|---|---|---|
writeINF |
cyan | General informational message |
writeDBG |
grey | Only printed when $DEBUG or $debug is set |
writeWRN |
yellow | Includes source file and line number |
writeERR |
red | Includes source file and line number |
writeOK |
green | Single-line pass / success result |
writeFAIL |
red | Single-line fail / validation result |
writeNOTE |
grey | Single-line subtle annotation |
writeTODO |
yellow | Includes function name, file, and line number |
All functions accept a single string argument and support multi-line messages:
writeINF "Library loaded successfully."
writeINF \
"
Multi-line message:
line one
line two
"
writeWRN "Something unexpected happened."
writeERR "Fatal: could not connect."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