Skip to content

Instantly share code, notes, and snippets.

@szymonk92
Created October 26, 2025 13:09
Show Gist options
  • Select an option

  • Save szymonk92/0acd2d5a2f94fb698d05bb56bd5bacb1 to your computer and use it in GitHub Desktop.

Select an option

Save szymonk92/0acd2d5a2f94fb698d05bb56bd5bacb1 to your computer and use it in GitHub Desktop.
#!/usr/bin/env zsh
ports() {
autoload -U colors && colors
# Usage info
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
cat <<EOF
Usage: ports [OPTIONS] [FILTER]
Display listening ports with process and container information.
OPTIONS:
-j, --json Output as JSON (for scripts)
-w, --watch Refresh every 5 seconds
-v, --verbose Show full command lines
-W, --wide Wide mode - don't truncate output
-h, --help Show this help message
FILTER:
Text or port number to filter results
EXAMPLES:
ports # Show all listening ports
ports 3000 # Show only port 3000
ports node # Show all Node.js processes
ports -w # Watch mode (refresh every 5s)
ports -W # Wide mode (show full names)
ports -j 8080 # JSON output for port 8080
EOF
return
fi
# Parse arguments
local filter=""
local json_mode=false
local watch_mode=false
local verbose_mode=false
local wide_mode=false
while [[ $# -gt 0 ]]; do
case "$1" in
-j|--json)
json_mode=true
shift
;;
-w|--watch)
watch_mode=true
shift
;;
-v|--verbose)
verbose_mode=true
shift
;;
-W|--wide)
wide_mode=true
shift
;;
-h|--help)
# Already handled above
return
;;
*)
filter="$1"
shift
;;
esac
done
# Watch mode
if [[ "$watch_mode" == true ]]; then
local watch_cmd="zsh -c 'source ~/.zshrc; ports"
[[ "$json_mode" == true ]] && watch_cmd="$watch_cmd -j"
[[ "$verbose_mode" == true ]] && watch_cmd="$watch_cmd -v"
[[ "$wide_mode" == true ]] && watch_cmd="$watch_cmd -W"
[[ -n "$filter" ]] && watch_cmd="$watch_cmd \"$filter\"'"
watch -c -n 5 "$watch_cmd"
return
fi
# Build docker port mapping
declare -A docker_map
if command -v docker &>/dev/null; then
# Parse docker containers and their exposed ports
docker ps 2>/dev/null | tail -n +2 | while read -r line; do
local container_id=$(echo "$line" | awk '{print $1}')
local container_name=$(docker ps --format "{{.Names}}" -f "id=$container_id" 2>/dev/null)
local container_image=$(docker ps --format "{{.Image}}" -f "id=$container_id" 2>/dev/null)
local container_status=$(docker ps --format "{{.Status}}" -f "id=$container_id" 2>/dev/null)
local container_ports=$(docker ps --format "{{.Ports}}" -f "id=$container_id" 2>/dev/null)
# Parse each port mapping
echo "$container_ports" | tr ',' '\n' | while read -r port_spec; do
# Match patterns like "0.0.0.0:3000->3000/tcp" or ":::3000->3000/tcp"
if [[ "$port_spec" =~ ':([0-9]+)->([0-9]+)' ]]; then
local host_port="${match[1]}"
local container_port="${match[2]}"
docker_map[$host_port]="${container_name}|${container_image}|${container_status}|${container_port}"
fi
done
done
fi
# Collect and deduplicate port information
declare -A port_data
# Get listening ports
sudo lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | tail -n +2 | while read -r line; do
local address=$(echo "$line" | awk '{print $9}')
local pid=$(echo "$line" | awk '{print $2}')
[[ -z "$pid" ]] && continue
# Extract port number
local port=""
if [[ "$address" =~ ':([0-9]+)$' ]]; then
port="${match[1]}"
else
continue
fi
# Get process information
local cmd=$(ps -p "$pid" -o command= 2>/dev/null | tr -d '\n')
[[ -z "$cmd" ]] && continue
# Create unique key (prefer specific addresses over wildcards)
local key="${port}_${pid}"
# Store with priority (specific addresses override wildcards)
if [[ -z "${port_data[$key]}" ]] || [[ "$address" != "*:"* ]]; then
port_data[$key]="${address}|${pid}|${cmd}"
fi
done
# Header for table mode
if [[ "$json_mode" != true ]]; then
printf "\n${fg_bold[white]}%-22s %-8s %-12s %s${reset_color}\n" "PORT" "PID" "TYPE" "COMMAND / CONTAINER"
printf "${fg_bold[black]}%s${reset_color}\n" "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
fi
# Process and display results
for key in "${(@k)port_data}"; do
IFS='|' read -r address pid cmd <<< "${port_data[$key]}"
# Extract port for docker lookup
local port=""
if [[ "$address" =~ ':([0-9]+)$' ]]; then
port="${match[1]}"
fi
# Determine type and format display
local type="system"
local display="$cmd"
local color="${reset_color}"
# Check for Docker container
if [[ "$cmd" == *"com.docker.backend"* ]] && [[ -n "${docker_map[$port]}" ]]; then
IFS='|' read -r name image cont_status container_port <<< "${docker_map[$port]}"
# Format the display based on available space
if [[ "$wide_mode" == true ]] || [[ "$verbose_mode" == true ]]; then
display="๐Ÿณ ${name} โ†’ :${container_port} (${image}, ${cont_status})"
else
# Show name and image, truncate image if needed
local short_image="${image}"
if [[ "${#image}" -gt 30 ]]; then
# Try to keep the tag if present
if [[ "$image" == *":"* ]]; then
local image_name="${image%:*}"
local image_tag="${image#*:}"
if [[ "${#image_name}" -gt 20 ]]; then
short_image="...${image_name: -17}:${image_tag}"
else
short_image="${image_name}:${image_tag}"
fi
else
short_image="...${image: -27}"
fi
fi
display="๐Ÿณ ${name} โ†’ :${container_port} (${short_image})"
fi
type="docker"
color="${fg_bold[green]}"
elif [[ "$cmd" == *"com.docker.backend"* ]]; then
display="๐Ÿณ Docker Desktop (awaiting container)"
type="docker"
color="${fg[yellow]}"
# Node.js processes
elif [[ "$cmd" == *"node"* ]]; then
type="node"
color="${fg_bold[cyan]}"
if [[ "$verbose_mode" != true ]]; then
# Extract main script name
local script=""
if [[ "$cmd" =~ 'node[[:space:]]+([^[:space:]]+)' ]]; then
script="${match[1]}"
# Get just the filename if it's a path
script="${script:t}"
display="๐Ÿ“ฆ node: ${script}"
elif [[ "$cmd" == *"vite"* ]]; then
display="โšก Vite dev server"
elif [[ "$cmd" == *"expo"* ]]; then
display="๐Ÿ“ฑ Expo dev server"
elif [[ "$cmd" == *"webpack"* ]]; then
display="๐Ÿ“ฆ Webpack dev server"
elif [[ "$cmd" == *"next"* ]]; then
display="โ–ฒ Next.js dev server"
else
display="๐Ÿ“ฆ Node.js process"
fi
fi
# Python processes
elif [[ "$cmd" == *"python"* ]]; then
type="python"
color="${fg_bold[blue]}"
if [[ "$verbose_mode" != true ]]; then
if [[ "$cmd" == *"django"* ]] || [[ "$cmd" == *"manage.py"* ]]; then
display="๐Ÿ Django server"
elif [[ "$cmd" == *"flask"* ]]; then
display="๐Ÿ Flask server"
elif [[ "$cmd" == *"uvicorn"* ]] || [[ "$cmd" == *"fastapi"* ]]; then
display="๐Ÿ FastAPI server"
else
display="๐Ÿ Python process"
fi
fi
# Java processes
elif [[ "$cmd" == *"java"* ]]; then
type="java"
color="${fg_bold[magenta]}"
if [[ "$verbose_mode" != true ]]; then
if [[ "$cmd" == *"spring"* ]]; then
display="โ˜• Spring Boot"
elif [[ "$cmd" == *"tomcat"* ]]; then
display="โ˜• Tomcat server"
elif [[ "$cmd" == *"gradle"* ]]; then
display="โ˜• Gradle build"
else
display="โ˜• Java application"
fi
fi
# Ruby processes
elif [[ "$cmd" == *"ruby"* ]] || [[ "$cmd" == *"rails"* ]]; then
type="ruby"
color="${fg_bold[red]}"
if [[ "$verbose_mode" != true ]]; then
if [[ "$cmd" == *"rails"* ]]; then
display="๐Ÿ’Ž Rails server"
else
display="๐Ÿ’Ž Ruby process"
fi
fi
# Database processes
elif [[ "$cmd" == *"postgres"* ]] || [[ "$cmd" == *"postgresql"* ]]; then
type="postgres"
color="${fg_bold[blue]}"
display="๐Ÿ˜ PostgreSQL"
elif [[ "$cmd" == *"mysql"* ]] || [[ "$cmd" == *"mariadb"* ]]; then
type="mysql"
color="${fg_bold[blue]}"
display="๐Ÿฌ MySQL/MariaDB"
elif [[ "$cmd" == *"mongod"* ]]; then
type="mongodb"
color="${fg_bold[green]}"
display="๐Ÿƒ MongoDB"
elif [[ "$cmd" == *"redis"* ]]; then
type="redis"
color="${fg_bold[red]}"
display="๐Ÿ“ฎ Redis"
# Other common services
elif [[ "$cmd" == *"nginx"* ]]; then
type="nginx"
color="${fg_bold[green]}"
display="๐ŸŒ Nginx"
elif [[ "$cmd" == *"apache"* ]] || [[ "$cmd" == *"httpd"* ]]; then
type="apache"
color="${fg_bold[red]}"
display="๐Ÿชถ Apache"
# Common Mac/Desktop applications
elif [[ "$cmd" == *"Spotify"* ]]; then
type="spotify"
color="${fg_bold[green]}"
display="๐ŸŽต Spotify"
elif [[ "$cmd" == *"Discord"* ]]; then
type="discord"
color="${fg_bold[blue]}"
display="๐Ÿ’ฌ Discord"
elif [[ "$cmd" == *"Slack"* ]]; then
type="slack"
color="${fg_bold[magenta]}"
display="๐Ÿ’ผ Slack"
elif [[ "$cmd" == *"Code Helper"* ]] || [[ "$cmd" == *"Visual Studio Code"* ]]; then
type="vscode"
color="${fg_bold[blue]}"
display="๐Ÿ“ VS Code Extension Host"
elif [[ "$cmd" == *"IntelliJ"* ]] || [[ "$cmd" == *"idea"* ]]; then
type="intellij"
color="${fg_bold[magenta]}"
display="๐Ÿง  IntelliJ IDEA"
elif [[ "$cmd" == *"Canary Mail"* ]]; then
type="email"
color="${fg_bold[yellow]}"
display="๐Ÿ“ง Canary Mail"
elif [[ "$cmd" == *"ControlCenter"* ]]; then
type="system"
color="${fg[black]}"
display="โš™๏ธ macOS Control Center"
elif [[ "$cmd" == *"rapportd"* ]]; then
type="system"
color="${fg[black]}"
display="๐Ÿ“ก macOS Handoff/Continuity"
elif [[ "$cmd" == *"launchd"* ]]; then
type="system"
color="${fg[black]}"
display="๐Ÿš€ macOS launchd"
elif [[ "$cmd" == *"adb"* ]]; then
type="android"
color="${fg_bold[green]}"
display="๐Ÿค– Android Debug Bridge"
# System services - show cleaner names
elif [[ "$verbose_mode" != true ]]; then
# Extract just the binary name for system processes
local binary="${cmd%% *}"
binary="${binary:t}"
display="๐Ÿ”ง ${binary}"
fi
# Shorten display if too long and not verbose or wide mode
if [[ "$verbose_mode" != true ]] && [[ "$wide_mode" != true ]]; then
if [[ "${#display}" -gt 100 ]]; then
display="${display:0:97}..."
fi
fi
# Format address for display
local addr_display="$address"
addr_display="${addr_display//\*:/:}"
addr_display="${addr_display//0.0.0.0:/:}"
addr_display="${addr_display//127.0.0.1:/localhost:}"
addr_display="${addr_display//\[::1\]:/localhost:}"
addr_display="${addr_display//\[::\]/:}"
# Apply filter
if [[ -n "$filter" ]]; then
local full_line="${addr_display} ${pid} ${type} ${display}"
[[ "$full_line" != *"$filter"* ]] && continue
fi
# Output
if [[ "$json_mode" == true ]]; then
# JSON output
local json_display="${display//\"/\\\"}"
printf '{"port":"%s","pid":"%s","type":"%s","command":"%s"}\n' \
"$addr_display" "$pid" "$type" "$json_display"
else
# Table output
printf "%-22s %-8s %-12s ${color}%s${reset_color}\n" \
"$addr_display" "$pid" "$type" "$display"
fi
done | {
# Sort by port number
if [[ "$json_mode" == true ]]; then
cat
else
sort -t: -k2 -n
fi
}
[[ "$json_mode" != true ]] && echo "" # Add newline at end
}
# Add alias for convenience
alias p='ports'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment