Created
October 26, 2025 13:09
-
-
Save szymonk92/0acd2d5a2f94fb698d05bb56bd5bacb1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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