A minimalist bash application framework that makes building CLI tools delightfully simple.
Undies embraces bash's strengths while hiding its weirdness. Write simple functions, declare parameters, and get a fully-featured CLI app with help text, parameter parsing, and task execution—all without learning bash quirks.
Create a file hello.sh:
__message=${HELLO__message='Hello, World!'}
greet__help="@arg name @does greet someone"
greet__ () {
echo ${1:-$__message}
}
. undiesMake it executable and run:
$ chmod +x hello.sh
$ ./hello.sh
# Shows help with available tasks and parameters
$ ./hello.sh greet
Hello, World!
$ ./hello.sh __message="Hey there!" greet
Hey there!
$ ./hello.sh greet "Alice"
AliceTasks are bash functions ending with double underscore __:
deploy__help="@does deploy the application"
deploy__ () {
echo "Deploying..."
}Parameters use a naming convention that enables defaults and environment overrides:
__port=${MYAPP__port='8080'}
__host=${MYAPP__host='localhost'}__port- the variable used in your codeMYAPP__port- environment variable for override'8080'- default value
Document tasks with help variables:
task__help="@arg argument @does description of what this task does"$ ./app.sh task
$ ./app.sh __param=value task
$ ./app.sh __param1=value1 __param2=value2 task$ ./app.sh help # Show all tasks
$ ./app.sh help taskname # Show specific task details
$ ./app.sh # Shows help by default$ undies -- script1.sh -- script2.sh
$ undies -- __port=3000 app.sh startUndies includes built-in bash autocomplete for all your tasks and parameters. Enable it with one command:
$ ./myapp.sh __install
$ source ~/.bashrcNow enjoy tab completion:
$ ./myapp.sh <TAB> # Shows all tasks
$ ./myapp.sh __<TAB> # Shows all parametersFor detailed autocomplete documentation, see docs/Auto_Completion.md.
Override these functions to add custom behavior:
__puton__help="@does runs before any task"
__puton__ () {
echo "Starting task: $__task__"
}
__takeoff__help="@does runs after successful task completion"
__takeoff__ () {
echo "Completed task: $__task__"
}
__fallback__help="@does runs when task fails or is not found"
__fallback__ () {
echo "Task failed: $__task__"
}Define a custom default when no task is specified:
__default__help="@does show status"
__default__ () {
echo "Application status: running"
}Undies provides helper functions you can use in your tasks:
Execute commands with built-in display and confirmation:
deploy__ () {
__exec "git pull origin main"
__exec "npm install"
__exec "npm run build"
}The user will see each command and can confirm before execution.
Check if a task function exists:
if __exists backup__; then
backup__
fiGet a list of all available tasks:
list__help="@does list all available tasks"
list__ () {
for task in $(__tasks); do
echo "- $task"
done
}Get a list of all declared parameters:
show_config__help="@does show current configuration"
show_config__ () {
echo "Current parameters:"
__params
}Get the application name:
version__help="@does show application info"
version__ () {
echo "$(__app) version 1.0"
}Simple string templating for output:
declare -A vars
vars[name]="Alice"
vars[age]="30"
t "Hello, @name! You are @age years old." vars
# Output: Hello, Alice! You are 30 years old.Undies exposes variables you can use in your tasks:
The full path to the current script file:
backup__help="@does backup this script"
backup__ () {
cp $__src__ $__src__.backup
echo "Backed up to $__src__.backup"
}The directory containing the current script:
# Source additional libraries relative to your script
source $__path__/lib.sh
source $__path__/config.sh
# Access data files
cat $__path__/data/users.txtThis works even when the script is run from a different directory:
$ cd /tmp
$ /home/user/myapp/app.sh task
# $__path__ will be /home/user/myappThe name of the currently executing task:
__puton__ () {
echo "Running task: $__task__"
}The Undies API version (currently 4):
if [[ $__API__ -lt 4 ]]; then
echo "This script requires Undies API version 4 or higher"
exit 1
fi#!/usr/bin/env bash
__file=${TODO__file="$HOME/.todos"}
add__help="@arg task @does add a new todo"
add__ () {
echo "- [ ] $*" >> $__file
echo "Added: $*"
}
list__help="@does list all todos"
list__ () {
cat $__file 2>/dev/null || echo "No todos yet!"
}
done__help="@arg number @does mark todo as complete"
done__ () {
sed -i "${1}s/\[ \]/[x]/" $__file
echo "Marked todo $1 as complete"
}
. undies#!/usr/bin/env bash
__env=${DEPLOY__env='staging'}
__branch=${DEPLOY__branch='main'}
deploy__help="@does deploy application to specified environment"
deploy__ () {
__exec "git fetch origin"
__exec "git checkout $__branch"
__exec "git pull origin $__branch"
__exec "npm install"
__exec "npm run build"
__exec "rsync -av ./dist/ server-$__env:/var/www/"
__exec "ssh server-$__env 'systemctl restart app'"
}
rollback__help="@does rollback to previous deployment"
rollback__ () {
__exec "ssh server-$__env 'cd /var/www && git checkout HEAD~1'"
__exec "ssh server-$__env 'systemctl restart app'"
}
. undies#!/usr/bin/env bash
# main.sh
# Source libraries relative to script location
source $__path__/lib/database.sh
source $__path__/lib/helpers.sh
__db_host=${APP__db_host='localhost'}
migrate__help="@does run database migrations"
migrate__ () {
db_connect $__db_host
db_migrate
}
. undiescurl -o undies https://raw.githubusercontent.com/yourrepo/undies/main/undies
chmod +x undies
sudo mv undies /usr/local/bin/Download undies and source it at the end of your script:
#!/usr/bin/env bash
# Your tasks here
task__ () {
echo "Hello"
}
# Source undies
. /path/to/undiesundies -- /path/to/your-app.sh taskThe following features are experimental and may change in future versions:
Use __dryrun parameter to preview commands without executing:
$ ./app.sh __dryrun=1 deploy
# Commands will be shown but not executedUse __verbose parameter for detailed logging:
__verbose=${MYAPP__verbose='0'}
deploy__ () {
[[ $__verbose -ge 1 ]] && echo "[INFO] Starting deployment"
[[ $__verbose -ge 2 ]] && echo "[DEBUG] Checking prerequisites"
}$ ./app.sh __verbose=2 deployOrganize related tasks with double underscores:
db__migrate__help="@does run database migrations"
db__migrate__ () {
echo "Migrating database..."
}
db__seed__help="@does populate database with test data"
db__seed__ () {
echo "Seeding database..."
}$ ./app.sh db__migrate
$ ./app.sh db__seedCustom error codes for specific failure conditions:
_err_fn_na=101 # Function does not existHere are patterns you might want to implement in your undies applications:
Load configuration from .env files:
__env=${MYAPP__env='development'}
# In your script initialization
[[ -f $__path__/.env ]] && source $__path__/.env
[[ -f $__path__/.env.$__env ]] && source $__path__/.env.$__env
start__help="@does start the application"
start__ () {
echo "Starting in $__env environment"
}Usage:
$ ./app.sh __env=production startRun prerequisite tasks before main tasks:
build__help="@does compile the application"
build__ () {
echo "Building..."
}
deploy__help="@does deploy (builds first)"
deploy__ () {
build__ # Run build task first
echo "Deploying..."
}Validate parameters before running tasks:
__port=${MYAPP__port='8080'}
__puton__ () {
if [[ $__port -lt 1 || $__port -gt 65535 ]]; then
echo "Error: port must be between 1 and 65535"
return 1
fi
}Store and load configuration:
config__help="@does show current configuration"
config__ () {
echo "Configuration from: $__path__/config"
cat $__path__/config 2>/dev/null || echo "No config file found"
}
save_config__help="@does save current parameters to config file"
save_config__ () {
__params > $__path__/config
echo "Configuration saved"
}- Simple: Write bash functions, get a CLI tool
- Consistent: Standard conventions for parameters and tasks
- Documented: Auto-generated help from your code
- Portable: Pure bash, works everywhere
- Minimal: ~200 lines of bash, zero dependencies
GPL v3
Contributions welcome! Please open an issue or pull request.
The entire source code is human-made. This readme file is made by Claude Sonnet 4.5. (with minor tweeks)