Automate repetitive tasks with a tiny Python CLI
Welcome to another official UVF IT build guide. In this tutorial we’ll craft a minimal command‑line utility that mimics the spirit of the Superpowers project – a tiny script that runs user‑defined “powers” (shell commands) from a JSON catalog.
You’ll finish a working clone in about 45 minutes using only Python’s standard library and a local SQLite DB – no paid APIs, no external services.
git to clone the repo.cmd syntax for the commands you store; on Unix use typical shell syntax.First we create a clean directory for our clone and isolate its dependencies with a virtual environment. This keeps the project lightweight and reproducible on any machine.
Next we initialise a tiny SQLite database that will hold a single power table – each row describes a named shell command that the CLI can execute later. All of this is done with plain Bash commands, no extra tools required.
# Create project folder and enter it
mkdir superpowers_clone && cd superpowers_clone
# Set up an isolated Python environment
python3 -m venv .venv
source .venv/bin/activate
# Upgrade pip (optional but recommended)
pip install --upgrade pip
# Initialise SQLite DB with a simple table
sqlite3 powers.db <<'SQL'
CREATE TABLE IF NOT EXISTS power (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
command TEXT NOT NULL
);
SQL
# Verify the table exists
sqlite3 powers.db "SELECT name FROM sqlite_master WHERE type='table' AND name='power';"Now we write a tiny Python module that abstracts the raw SQLite calls. The module provides list_powers, add_power, and run_power helpers so the CLI stays clean and focused on argument parsing.
All operations use the standard sqlite3 library, so there are no external dependencies beyond the virtual environment we created earlier.
model.py
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).with_name('powers.db')
def _connect():
"""Open a connection to the SQLite DB and enforce row access by name."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def list_powers():
"""Return a list of all stored powers as dicts {id, name, command}."""
with _connect() as conn:
cur = conn.execute('SELECT id, name, command FROM power ORDER BY name')
return [dict(row) for row in cur]
def add_power(name: str, command: str):
"""Insert a new power; raises sqlite3.IntegrityError if name exists."""
with _connect() as conn:
conn.execute('INSERT INTO power (name, command) VALUES (?, ?)', (name, command))
conn.commit()
def get_power(name: str):
"""Fetch a single power by name; returns None if not found."""
with _connect() as conn:
cur = conn.execute('SELECT command FROM power WHERE name = ?', (name,))
row = cur.fetchone()
return row['command'] if row else NoneIn this step we create the user‑facing command line interface. We'll use Python's built‑in argparse module to expose three actions: add a new power, list all stored powers, and run a selected power. The script talks to the SQLite database created earlier, inserting or reading rows as needed. Keeping everything in one file (cli.py) makes the clone easy to run and extend.
cli.py
#!/usr/bin/env python3
import argparse, sqlite3, subprocess, sys
DB='powers.db'
def init_db():
conn=sqlite3.connect(DB)
conn.execute('CREATE TABLE IF NOT EXISTS power (name TEXT PRIMARY KEY, command TEXT)')
conn.commit()
conn.close()
def add_power(name, cmd):
conn=sqlite3.connect(DB)
try:
conn.execute('INSERT INTO power (name, command) VALUES (?,?)',(name,cmd))
conn.commit()
except sqlite3.IntegrityError:
print(f"Power '{name}' already exists.")
finally:
conn.close()
def list_powers():
conn=sqlite3.connect(DB)
for row in conn.execute('SELECT name FROM power'):
print(row[0])
conn.close()
def run_power(name):
conn=sqlite3.connect(DB)
cur=conn.execute('SELECT command FROM power WHERE name=?',(name,))
row=cur.fetchone()
conn.close()
if not row:
print(f"No power named '{name}'.")
sys.exit(1)
subprocess.run(row[0], shell=True)
def main():
init_db()
parser=argparse.ArgumentParser(prog='cli.py', description='Superpowers CLI')
sub=parser.add_subparsers(dest='action')
add=sub.add_parser('add', help='Add a new power')
add.add_argument('name')
add.add_argument('command')
sub.add_parser('list', help='List all powers')
run=sub.add_parser('run', help='Run a power')
run.add_argument('name')
args=parser.parse_args()
if args.action=='add':
add_power(args.name, args.command)
elif args.action=='list':
list_powers()
elif args.action=='run':
run_power(args.name)
else:
parser.print_help()
if __name__=='__main__':
main()Now we make the script executable and try a couple of commands to see it in action. First, we add a simple greeting power, then list it, and finally run it. These steps prove the CLI talks to the SQLite store and can execute shell commands.
chmod +x cli.py
# Add a new power called 'greet' that echoes a friendly message
./cli.py add greet "echo 'Hello, UVF IT!'"
# List all stored powers to confirm the addition
./cli.py list
# Run the newly added power
./cli.py run greetWe verify that the output matches our expectations and then think about the next enhancements. The basic clone works, but you might want to store more metadata (description, tags) or persist the JSON format used by the original Superpowers project. Feel free to edit cli.py to accept additional arguments or to load a JSON file on startup.
# Verify the table exists
sqlite3 powers.db "SELECT name FROM sqlite_master WHERE type='table' AND name='power';"
# Verify the added power appears in the list output
./cli.py list | grep greet
# Verify running the power prints the expected greeting
./cli.py run greet | grep 'Hello, UVF IT!'| Check | Command / Why it matters |
|---|---|
SQLite DB created with power table | sqlite3 powers.db "SELECT name FROM sqlite_master WHERE type='table' AND name='power';" |
| Added power appears in list | ./cli.py list | grep greet |
| Running power prints expected text | ./cli.py run greet | grep 'Hello, UVF IT!' |
You now have a functional command‑line “Superpowers” clone. Extend it by adding argument templating, a tiny web UI, or integrating a free‑tier LLM for dynamic command generation – all while staying on the same free stack.