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 thingsargparsegives you for free.
/ Programming / Python / Tutorial / Command-line