The Human's Guide to the Command Line: Your First CLI App

This is Part 2 of The Human’s Guide to the Command Line. If you missed Part 1, go check that out first — we got Homebrew and Ghostty set up, which you’ll need for everything here.

Now we’re going to do something that feels like a big leap: we’re going to write a real command-line application. A small one, but a real one. Before we get there, though, we need two things — a programming language and a code editor.

Picking a Language

You can write CLI apps in a lot of languages. JavaScript, Go, Rust — they all work. But if you’re new to programming, I think Python is the right starting point. It reads almost like English, it’s everywhere, and you won’t spend your first hour fighting a compiler.

Python is what we’ll use for this series. That said, Python is notoriously tricky to set up locally; there are version managers, virtual environments, and a whole ecosystem of packaging tools that can make your head spin.

First things first, we need a code editor (an IDE).

A Code Editor: Zed

Before we write any code, you need somewhere to write it. We’re going to install Zed — it’s fast, clean, and won’t overwhelm you with buttons.

brew install --cask zed

Open Zed from your Applications folder once it finishes.

Install the zed Command

Here’s the move that ties everything together. Open Zed’s command palette with Cmd + Shift + P, type cli install, and hit Enter.

Now you can open any folder in Zed directly from Ghostty:

zed .        # open the current folder
zed note.py  # open a specific file

That . means “right here” — you’ll see it everywhere in the terminal and it always means the same thing: the folder you’re currently in.

Set Up Your Project

Time to create a home for your code. Back in Ghostty:

mkdir ~/code
mkdir ~/code/note
cd ~/code/note

Three commands. You just created a code folder in your home directory, created a note folder inside it, and stepped into it. Now open it in Zed:

zed .

Zed opens with your empty project folder in the sidebar on the left. This is where we’ll create our script.

Building a Note Taker

We’re going to build a tiny app called note. It does three things: add a note, list your notes, and clear them all. That’s it. No database, no accounts, no cloud. Just you and a JSON file.

Create your script in Zed’s sidebar by clicking the New File icon, name it note.py, and paste in the code from the next section.

import argparse
import json
import sys
from datetime import datetime
from pathlib import Path

# Where your notes live — ~/notes.json
NOTES_FILE = Path.home() / "notes.json"


def load_notes():
    if not NOTES_FILE.exists():
        return []
    with open(NOTES_FILE) as f:
        return json.load(f)


def save_notes(notes):
    with open(NOTES_FILE, "w") as f:
        json.dump(notes, f, indent=2)


def cmd_add(args):
    notes = load_notes()
    note = {
        "text": args.text,
        "added": datetime.now().strftime("%Y-%m-%d %H:%M"),
    }
    notes.append(note)
    save_notes(notes)
    print(f"Added: {args.text}")


def cmd_list(args):
    notes = load_notes()
    if not notes:
        print('No notes yet. Add one with: note add "your note"')
        return
    for i, note in enumerate(notes, start=1):
        print(f"{i}. {note['text']}  ({note['added']})")


def cmd_clear(args):
    answer = input("Delete all notes? This can't be undone. (y/n): ")
    if answer.lower() == "y":
        save_notes([])
        print("All notes cleared.")
    else:
        print("Cancelled.")


def main():
    parser = argparse.ArgumentParser(
        prog="note",
        description="A simple command-line note taker.",
    )
    subparsers = parser.add_subparsers(dest="command")

    # note add "some text"
    add_parser = subparsers.add_parser("add", help="Add a new note")
    add_parser.add_argument("text", help="The note to save")

    # note list
    subparsers.add_parser("list", help="Show all notes")

    # note clear
    subparsers.add_parser("clear", help="Delete all notes")

    args = parser.parse_args()

    if args.command == "add":
        cmd_add(args)
    elif args.command == "list":
        cmd_list(args)
    elif args.command == "clear":
        cmd_clear(args)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

Part 3: Python Without the Mess (uv)

Your Mac already has Python on it, but don’t use it. That version belongs to macOS which it uses internally, and if you start installing things into it you can cause yourself real headaches. We’re going to use our own Python that’s completely separate.

The tool for this is uv. It manages Python for you and keeps everything isolated so you never touch the system version.

brew install uv

That’s the whole install. Verify it worked:

uv --version

You should see a version number printed back. If you do, you’re good.

Create Your Project

Inside your ~/code/note folder, run:

uv init

This sets up a proper Python project in the current folder. You’ll see a few new files appear in Zed’s sidebar, but don’t worry. If you see a hello.py, which uv creates as a starter file, you can delete it.

Running Your Script

Once you have code in note.py, run it like this:

uv run note.py add "call the dentist"
uv run note.py list
uv run note.py clear

uv run handles everything — it picks the right Python version, keeps it sandboxed to this project, and runs your script. You never type python3 directly, you never activate a virtual environment, you never install packages globally. It just works.

Try it now:

uv run note.py list

If you see No notes yet. Add one with: note add "your note" but otherwise, everything should be working.


Try this: Once your script is running, try uv run note.py --help. You’ll get a clean description of every command, automatically. That’s one of the things argparse gives you for free.

/ Programming / Python / Tutorial / Command-line