#!/usr/bin/env bash ######################################################################################### #### port-checker.sh — Check host:port pairs and report open/closed/timeout status #### #### Accepts targets from CLI args, a file, or stdin #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.00 #### #### #### #### Usage: #### #### ./port-checker.sh host1:port1 host2:port2 #### #### ./port-checker.sh --file targets.txt #### #### echo "host:port" | ./port-checker.sh #### #### #### #### See --help for all options. #### ######################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────── TIMEOUT="${PORT_CHECK_TIMEOUT:-3}" VERBOSE="${VERBOSE:-false}" COLOR="${COLOR:-auto}" JSON_OUTPUT="${JSON_OUTPUT:-false}" INPUT_FILE="" # ── State ───────────────────────────────────────────────────────────── SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME TARGETS=() COUNT_OPEN=0 COUNT_CLOSED=0 COUNT_TIMEOUT=0 COUNT_TOTAL=0 # ── Colors ──────────────────────────────────────────────────────────── setup_colors() { if [[ "$COLOR" == "never" ]]; then RED="" GREEN="" YELLOW="" BOLD="" DIM="" RESET="" return fi if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED="" GREEN="" YELLOW="" BOLD="" DIM="" RESET="" fi } # ── Logging ─────────────────────────────────────────────────────────── log() { echo -e "${DIM}[INFO]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; } err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; } # ── Helpers ─────────────────────────────────────────────────────────── check_port() { local host="$1" local port="$2" local status="closed" local latency="-" verbose "Checking ${host}:${port} (timeout ${TIMEOUT}s)" local start_ns end_ns elapsed_ms start_ns=$(date +%s%N) if command -v nc &>/dev/null; then if nc -z -w "$TIMEOUT" "$host" "$port" &>/dev/null; then status="open" fi elif command -v ncat &>/dev/null; then if ncat -z -w "$TIMEOUT" "$host" "$port" &>/dev/null; then status="open" fi else if (echo >/dev/tcp/"$host"/"$port") 2>/dev/null; then status="open" fi fi end_ns=$(date +%s%N) elapsed_ms=$(( (end_ns - start_ns) / 1000000 )) if [[ "$status" == "closed" && "$elapsed_ms" -ge $(( TIMEOUT * 1000 )) ]]; then status="timeout" fi if [[ "$status" != "closed" || "$elapsed_ms" -lt $(( TIMEOUT * 1000 )) ]]; then latency="${elapsed_ms}ms" fi COUNT_TOTAL=$((COUNT_TOTAL + 1)) case "$status" in open) COUNT_OPEN=$((COUNT_OPEN + 1)) ;; closed) COUNT_CLOSED=$((COUNT_CLOSED + 1)) ;; timeout) COUNT_TIMEOUT=$((COUNT_TIMEOUT + 1)) ;; esac if [[ "$JSON_OUTPUT" == "true" ]]; then local json_comma="" if [[ "$COUNT_TOTAL" -gt 1 ]]; then json_comma="," fi printf '%s{"host":"%s","port":%s,"status":"%s","latency":"%s"}' \ "$json_comma" "$host" "$port" "$status" "$latency" else local color case "$status" in open) color="$GREEN" ;; closed) color="$RED" ;; timeout) color="$YELLOW" ;; *) color="" ;; esac printf " %-30s %-8s %b%-10s%b %s\n" "$host" "$port" "$color" "$status" "$RESET" "$latency" fi } load_targets_from_file() { local file="$1" if [[ ! -f "$file" ]]; then err "File not found: $file" exit 1 fi while IFS= read -r line; do line="${line%%#*}" line="${line// /}" [[ -z "$line" ]] && continue TARGETS+=("$line") done < "$file" } load_targets_from_stdin() { while IFS= read -r line; do line="${line%%#*}" line="${line// /}" [[ -z "$line" ]] && continue TARGETS+=("$line") done } # ══════════════════════════════════════════════════════════════════════ # USAGE # ══════════════════════════════════════════════════════════════════════ usage() { cat <&2 exit 1 ;; *) TARGETS+=("$1"); shift ;; esac done } # ══════════════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════════════ main() { parse_args "$@" setup_colors # Load targets from file if specified if [[ -n "$INPUT_FILE" ]]; then load_targets_from_file "$INPUT_FILE" fi # Load from stdin if no targets yet and stdin is not a terminal if [[ ${#TARGETS[@]} -eq 0 ]] && ! [[ -t 0 ]]; then load_targets_from_stdin fi # Validate we have targets if [[ ${#TARGETS[@]} -eq 0 ]]; then err "No targets specified" echo "Run ${SCRIPT_NAME} --help for usage" >&2 exit 1 fi if [[ "$JSON_OUTPUT" == "true" ]]; then printf '{"results":[' else echo "" echo -e "${BOLD}Port Checker${RESET}" echo -e "${DIM}Timeout: ${TIMEOUT}s${RESET}" echo "" printf " ${BOLD}%-30s %-8s %-10s %s${RESET}\n" "HOST" "PORT" "STATUS" "LATENCY" printf " %s\n" "$(printf '%.0s─' {1..62})" fi for target in "${TARGETS[@]}"; do local host port if [[ "$target" == *:* ]]; then host="${target%%:*}" port="${target##*:}" else warn "Invalid format (expected host:port): $target" continue fi if [[ -z "$host" || -z "$port" ]]; then warn "Invalid target: $target" continue fi check_port "$host" "$port" done if [[ "$JSON_OUTPUT" == "true" ]]; then printf '],"summary":{"total":%d,"open":%d,"closed":%d,"timeout":%d}}\n' \ "$COUNT_TOTAL" "$COUNT_OPEN" "$COUNT_CLOSED" "$COUNT_TIMEOUT" else echo "" echo -e " ${BOLD}Summary${RESET}" printf " %-14s %d\n" "Total checked:" "$COUNT_TOTAL" printf " %-14s %b%d%b\n" "Open:" "$GREEN" "$COUNT_OPEN" "$RESET" printf " %-14s %b%d%b\n" "Closed:" "$RED" "$COUNT_CLOSED" "$RESET" printf " %-14s %b%d%b\n" "Timeout:" "$YELLOW" "$COUNT_TIMEOUT" "$RESET" echo "" fi } main "$@"