I made a CLI tool called ports-cli
Why I made it
When developing, I often find myself asking "what's using this port?" Especially at work, I usually have multiple repo servers running at the same time.
Sure, lsof -i :3000 works, but the output is too verbose and I got tired of remembering the command every time.
So I made ports-cli. A CLI tool that shows listening ports in a clean format, along with process paths.
Features
ports # Show all listening ports
ports -u # Show only my processes
ports bye 3000 # Kill the process on port 3000
The output of ports -u looks like this:
PORT PROCESS PATH
---- ------- ----
3000 my-app ~/Documents/GitHub/my-app
5432 postgres -
8080 test-server ~/Documents/GitHub/test-server
What I wanted to see was: current open ports, app names, and their paths.
For Node processes, it reads the project name from package.json. Instead of just showing "node", it displays the actual project name, which I find much more intuitive.
Here's the development process by version:
v1.0.0 - Basic functionality
It started simple. Get listening ports via lsof, and if it's a Node process, parse the name from package.json.
# Get project name for Node processes
if [ "$proc" = "node" ]; then
cwd=$(lsof -p "$pid" 2>/dev/null | grep cwd | awk '{print $9}')
if [ -f "$cwd/package.json" ]; then
name=$(grep '"name"' "$cwd/package.json" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
if [ -n "$name" ]; then
proc="$name"
fi
fi
fi
v1.0.1 - Added -u option
But as I used it, a problem emerged. System processes like postgres, redis, and AirPlay were all showing up, making it hard to find my dev servers.
So I added the -u (--user) option.
It filters to show only processes the user directly started. Simply put, processes that have a path. System processes show - for PATH, so filtering by this lets me see only the Node processes I started.
if [ "$USER_ONLY" = true ]; then
[ "$display_path" = "-" ] && continue
[ -z "$display_path" ] && continue
fi
I struggled a bit here. I was tracking duplicate ports with declare -A seen_ports inside a while read loop, but it wasn't working...
The cause was a subshell issue with pipelines. When you do lsof | while read, the while loop runs in a subshell and variables don't get passed to the parent shell.
The solution was process substitution:
# This doesn't work (subshell issue)
lsof -iTCP -sTCP:LISTEN | while read line; do
...
done
# This is the way
while read line; do
...
done < <(lsof -iTCP -sTCP:LISTEN)
v1.2.0 - Added ports bye command
I also wanted to kill ports instantly. I got tired of copying PIDs and typing kill -9 every time, so I added the ports bye <port> command. Bye~
kill_port() {
local port=$1
local pid=$(lsof -ti TCP:$port -sTCP:LISTEN 2>/dev/null)
if [ -z "$pid" ]; then
echo "${RED}No process found on port $port${RESET}"
exit 1
fi
kill -9 $pid 2>/dev/null
if [ $? -eq 0 ]; then
echo "${GREEN}✓ Killed $proc_name (PID: $pid) on port $port${RESET}"
fi
}
Another bug... macOS's default bash is version 3.x, which doesn't support declare -A (associative arrays). Since I wanted to distribute via Homebrew, it needed to work with the default bash, so I changed it to string-based.
# Only works on bash 4.x+
declare -A seen_ports
seen_ports[$port]=1
# bash 3.x compatible
seen_ports=""
if echo "$seen_ports" | grep -q ":${port}:"; then
continue
fi
seen_ports="${seen_ports}:${port}:"
Since I made it, I wanted to distribute it via Homebrew. I'd never done it before, and since I made the project at work, I wanted an easy way to install it at home. Really simple reasons... but anyway...
- Create homebrew-tap repo
Create a homebrew-tap repo on GitHub and write the Formula/ports-cli.rb file:
class PortsCli < Formula
desc "Show listening ports with process names and paths"
homepage "https://github.com/givvemee/ports-cli"
url "https://github.com/givvemee/ports-cli/archive/refs/tags/v1.2.0.tar.gz"
sha256 "3fd0635de73b07fb87d6bdd533312e9b3fa2ab3ed286dacf2be58cf3c27d2e86"
license "MIT"
def install
bin.install "bin/ports"
end
test do
system "#{bin}/ports", "--version"
end
end
- Create release tag
git tag v1.2.0
git push origin v1.2.0
- Get sha256 hash
curl -sL https://github.com/givvemee/ports-cli/archive/refs/tags/v1.2.0.tar.gz | shasum -a 256
This is a bit tedious since you need to get a new hash and update the Formula every time you create a tag. But since there probably won't be many changes, I can live with a little inconvenience.
- Install
brew tap givvemee/tap
brew install ports-cli
Done! It's nothing fancy, but I'm using it well.