Every developer has that one small frustration they tolerate for too long.
Mine was navigating directories in the terminal. Typing cd projects/clients/awesome-project/backend/src/modules over and over, every single day. One typo and you start again.
I thought — why not just move to a directory by its index number? Like cd 3 instead of the full path.
That idea became index-shell — a Python CLI tool that overrides ls, cd, and nano in PowerShell to work with numeric indexes. This is the story of building it, the bugs I hit, and everything I wish I knew before starting.
How It Works
The concept is simple. Instead of this:
cd projects/backend/srcYou do this:
ls # shows numbered list of directories
cd 2 # moves into directory at index 2
nano 0 # opens file at index 0index-shell overrides the default PowerShell functions for ls, cd, and nano — injecting Python-powered versions that understand indexes.
The Core: Listing and Navigating by Index
The heart of the tool is core.py. It handles three things: listing directory contents with indexes, changing directory by index, and opening a file by index.
# core.py
import os
def list_visible(directory: str) -> list[str]:
return sorted(path for path in os.listdir(directory) if not path.startswith("."))
def list_dir(current_dir):
A few things worth noting:
list_visible()filters out hidden files (anything starting with.) and sorts the result — so your indexes are consistent every timechange_dir()returns the full path if valid, orNoneif not — the caller decides what to do with itopen_file()follows the same pattern aschange_dir()but validates it's a file, not a directoryos.path.join()handles path construction safely across platforms
The CLI Entry Point
cli.py is what actually runs when you type a command. It reads sys.argv, figures out which command was called, and routes to the right core function.
# cli.py
import sys
from index_shell.core import list_dir, change_dir, open_file
args = sys.argv[1:]
if not args:
print("Commands: ls | cd | nano")
command = args[0]
if command == "ls":
list_dir(args[
This is where I hit my first real bug.
Bug #1 — Getting sys.argv Right
sys.argv includes the script name as argv[0]. So when PowerShell calls:
python cli.py cd 2 C:\Users\project
The actual args are:
sys.argv = ['cli.py', 'cd', '2', 'C:\\Users\\project']After slicing with args = sys.argv[1:], index 0 is the command, index 1 is the numeric index, and index 2 is the current directory — passed automatically by the PowerShell wrapper.
Getting this wrong means either reading the wrong argument or crashing with an IndexError. The fix is simple once you understand it: always slice sys.argv[1:] first, then index from there.
The Tricky Part: Overriding PowerShell Functions
This is where things got interesting. To make cd 2 work in PowerShell, you can't just install a Python package — you need to override PowerShell's built-in cd and ls commands.
The solution is to inject custom functions into the PowerShell profile ($PROFILE) — a script that runs every time PowerShell starts.
shell.py handles this automatically. It builds the PowerShell script as a string, injects it into $PROFILE on setup, and cleanly removes it on uninstall using start/end markers:
# shell.py
import os
import subprocess
from pathlib import Path
import index_shell
package_dir = Path(index_shell.__file__).resolve().parent
script_path = package_dir / "cli.py"
is_Power_Shell = os.environ.get("PSModulePath")
PWSH_SCRIPT
A few important things happening here:
Remove-Alias cd -Force— PowerShell has a built-in alias forcd. You have to remove it before defining your own function, otherwise your function is ignored$PWD— PowerShell's current directory variable, passed automatically to the Python script so it knows where you are- The regex
'^\d+$'— checks if the argument is a pure number. If yes, use index navigation. If not, fall back to normalSet-Locationbehavior so regularcdstill works
Bug #2 — PowerShell Receiving Object[] Instead of a String
This was the bug that cost me the most time.
When the cd function ran, PowerShell was receiving Object[] instead of a plain directory path string. Set-Location would fail silently or throw a confusing error.
The cause? A stray print() statement left in the code during debugging.
When Python prints multiple times (even accidentally), PowerShell captures all the output as an array — an Object[]. It expects a single string. One extra print() anywhere in the call chain breaks the entire output protocol.
The fix: make sure cli.py prints exactly one thing to stdout when PowerShell is expecting a path — and nothing else. Debug prints go to stderr if needed:
import sys
# Use this for debug output, never plain print()
print("debug info", file=sys.stderr)This was the most important lesson from the whole project.
Publishing to PyPI
Once the tool worked locally, I wanted anyone to install it with:
pip install index-shellThe process requires a pyproject.toml:
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "index-shell"
version = "0.1.4"
requires-python = ">=3.8"
description = "Navigate your shell by index"
[project.scripts]
Then build and upload:
python -m build
python -m twine upload dist/*The issues I hit during publish:
- PATH problems —
twinewasn't recognized because Python'sScripts/folder wasn't in PATH. Fix: add it manually or usepython -m twineinstead - Network issues — PyPI upload timed out on first attempt. Just retry — it's usually transient
- Package name conflicts — your chosen name might already be taken on PyPI. Check at pypi.org before building
What I Wish I Knew Before Starting
1. Learn PowerShell syntax basics first.
I went in blind and spent hours on syntax errors in the profile script. Double braces {{}} for literal braces inside f-strings, param() blocks for function arguments, Set-Location vs cd — none of this is obvious coming from Python.
2. stdout is a communication channel, treat it seriously.
When your Python script is called by a shell function, every print() is data the shell receives. One accidental print breaks everything. Be intentional about what goes to stdout vs stderr.
3. Test the installed package, not just the local files. I wasted time debugging issues that only existed in my local dev setup. Install from PyPI (or a local build) and test that, because that's what your users get.
Install It
You can install index-shell directly from PyPI:
pip install index-shellThen run the setup command to inject the functions into your PowerShell profile:
index-shell-setupRestart PowerShell and you're good to go. To remove it:
index-shell-uninstallNote: This tool currently supports PowerShell only. Make sure you have PowerShell installed before running the setup command.
This Is a Work in Progress
index-shell is not fully polished yet. There are rough edges and I plan to improve it further — better error handling, Bash support, and more.
If you try it and have feedback, it genuinely matters. What's broken, what's confusing, what would make it more useful for you — drop a comment or reach out. That feedback will directly shape the next version.
One More Thing
This post is not a tutorial on how to build and publish a Python package. I walked through the code and the bugs, but the full process of structuring a package, writing pyproject.toml, and publishing to PyPI properly deserves its own post — which I'll cover in a future blog.
Stay tuned for that.
Final Thought
This was a small project but it taught me more about Python packaging, shell integration, and stdout handling than any tutorial did. The bugs were frustrating in the moment but that's exactly what made the lessons stick.
If you have a small repetitive pain in your workflow — build the fix. Then ship it. The process of going from idea to published package is worth doing at least once.