Skip to content

Instantly share code, notes, and snippets.

@SHSharkar
Last active August 19, 2025 11:58
Show Gist options
  • Select an option

  • Save SHSharkar/6a6eba963c2f0b7bcdea9d6c478c9a49 to your computer and use it in GitHub Desktop.

Select an option

Save SHSharkar/6a6eba963c2f0b7bcdea9d6c478c9a49 to your computer and use it in GitHub Desktop.
NetScope - Professional Network Quality Analysis Tool for macOS | Tests network performance using Apple's networkQuality with ISP-ready reports
#!/usr/bin/env bash
# =============================================================================
# NetScope - Network Quality Analysis Tool
# =============================================================================
# Description: Production-grade tool for generating comprehensive network
# quality reports for ISP debugging and troubleshooting
# Author: Md. Sazzad Hossain Sharkar
# Email: [email protected]
# Version: 1.2.0
# Created: 2025-08-19
# License: MIT
# =============================================================================
# Removed set -e for debugging
# =============================================================================
# CONFIGURATION & CONSTANTS
# =============================================================================
SCRIPT_NAME="$(basename "${0}")"
SCRIPT_VERSION="1.2.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TIMESTAMP="$(date '+%Y-%m-%d_%I-%M-%S_%p')"
LOG_DIR="${HOME}/Desktop/network-quality-reports"
CONFIG_DIR="${HOME}/.config/network-quality-reports"
CONFIG_FILE="${CONFIG_DIR}/config.env"
# API Configuration
IPAPI_BASE="http://ip-api.com/json"
IPAPI_TIMEOUT=10
NETWORK_QUALITY_TIMEOUT=60
# Test Configuration
DEFAULT_TEST_COUNT=3
DEFAULT_TEST_INTERVAL=30
MAX_TEST_COUNT=10
MIN_TEST_INTERVAL=10
# WhatsApp markdown formatting
WA_BOLD_START="*"
WA_BOLD_END="*"
WA_ITALIC_START="_"
WA_ITALIC_END="_"
WA_CODE_START="\`"
WA_CODE_END="\`"
WA_QUOTE_PREFIX="> "
# Colors for output (with NO_COLOR support)
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
RED=$'\033[0;31m'
GREEN=$'\033[0;32m'
YELLOW=$'\033[1;33m'
BLUE=$'\033[0;34m'
MAGENTA=$'\033[0;35m'
CYAN=$'\033[0;36m'
WHITE=$'\033[1;37m'
BOLD=$'\033[1m'
NC=$'\033[0m' # No Color
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
MAGENTA=''
CYAN=''
WHITE=''
BOLD=''
NC=''
fi
# =============================================================================
# GLOBAL VARIABLES
# =============================================================================
# IP API doesn't require token (free service)
OUTPUT_FORMAT="whatsapp"
VERBOSE=false
DEBUG=false
TEST_COUNT=${DEFAULT_TEST_COUNT}
TEST_INTERVAL=${DEFAULT_TEST_INTERVAL}
OUTPUT_FILE=""
INCLUDE_SYSTEM_INFO=true
USE_IPV6=false
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
# Logging functions
log_debug() {
[[ "${DEBUG}" == true ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $*" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
log_fatal() {
echo -e "${RED}[FATAL]${NC} $*" >&2
exit 1
}
# Progress indicator
show_progress() {
local current=$1
local total=$2
local message=$3
local percentage=$((current * 100 / total))
printf "\r${BLUE}[%3d%%]${NC} %s" "${percentage}" "${message}" >&2
}
# Spinner for long-running operations
spinner() {
local pid=$1
local message=$2
local spinchars="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
local i=0
while kill -0 "${pid}" 2>/dev/null; do
printf "\r${CYAN}%c${NC} %s" "${spinchars:i++%${#spinchars}:1}" "${message}"
sleep 0.1
done
printf "\r${GREEN}βœ“${NC} %s\n" "${message}"
}
# =============================================================================
# CONFIGURATION MANAGEMENT
# =============================================================================
create_directories() {
local dirs=("${LOG_DIR}" "${CONFIG_DIR}")
for dir in "${dirs[@]}"; do
if [[ ! -d "${dir}" ]]; then
log_debug "Creating directory: ${dir}"
mkdir -p "${dir}" || log_fatal "Failed to create directory: ${dir}"
fi
done
}
load_config() {
if [[ -f "${CONFIG_FILE}" ]]; then
log_debug "Loading configuration from: ${CONFIG_FILE}"
# shellcheck source=/dev/null
source "${CONFIG_FILE}"
else
log_debug "No configuration file found, using defaults"
fi
}
save_config() {
log_debug "Saving configuration to: ${CONFIG_FILE}"
cat >"${CONFIG_FILE}" <<EOF
# NetScope Configuration
# Generated on $(date)
# ip-api.com doesn't require API token (free service)
# Default test settings
DEFAULT_TEST_COUNT=${TEST_COUNT}
DEFAULT_TEST_INTERVAL=${TEST_INTERVAL}
# Output preferences
OUTPUT_FORMAT="${OUTPUT_FORMAT}"
INCLUDE_SYSTEM_INFO=${INCLUDE_SYSTEM_INFO}
USE_IPV6=${USE_IPV6}
EOF
}
# =============================================================================
# DEPENDENCY CHECKING
# =============================================================================
check_dependencies() {
local deps=("curl" "networkQuality" "system_profiler" "route")
local missing=()
log_debug "Checking dependencies..."
for dep in "${deps[@]}"; do
if ! command -v "${dep}" >/dev/null 2>&1; then
missing+=("${dep}")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_fatal "Missing required dependencies: ${missing[*]}"
fi
# Check macOS version for networkQuality
if ! networkQuality -h >/dev/null 2>&1; then
log_fatal "networkQuality command not available. Requires macOS 12.0+"
fi
log_debug "All dependencies satisfied"
}
# =============================================================================
# NETWORK INFORMATION GATHERING
# =============================================================================
get_dns_servers() {
# Get DNS servers but filter out local addresses (127.x.x.x and ::1)
scutil --dns 2>/dev/null | grep "nameserver\[0\]" | awk '{print $3}' |
grep -v '^127\.' | grep -v '^::1$' | head -3 | tr '\n' ', ' | sed 's/,$//' || echo "Unknown"
}
get_network_interface_info() {
local interface="${1:-$(route get default 2>/dev/null | grep interface | awk '{print $2}')}"
local interface_type="Unknown"
local link_speed="Unknown"
local hardware_port="Unknown"
if [[ -n "${interface}" ]]; then
# Get hardware port name from networksetup
hardware_port=$(networksetup -listallhardwareports 2>/dev/null | grep -B1 "Device: ${interface}" | grep "Hardware Port:" | cut -d: -f2 | xargs)
# Determine interface type and get link speed
if [[ "${hardware_port}" == *"Wi-Fi"* ]] || [[ "${hardware_port}" == *"AirPort"* ]]; then
interface_type="Wi-Fi"
# For Wi-Fi, get current link speed
link_speed=$(/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I 2>/dev/null | grep "lastTxRate:" | awk '{print $2 " Mbps"}')
if [[ -z "${link_speed}" ]] || [[ "${link_speed}" == " Mbps" ]]; then
link_speed="Connected"
fi
elif [[ "${hardware_port}" == *"Ethernet"* ]] || [[ "${hardware_port}" == *"LAN"* ]]; then
interface_type="Ethernet"
# Get Ethernet link speed from ifconfig
local media_info=$(ifconfig "${interface}" 2>/dev/null | grep "media:" | sed 's/.*(//' | sed 's/).*//')
if [[ "${media_info}" == *"1000baseT"* ]] || [[ "${media_info}" == *"1000Base"* ]]; then
link_speed="1 Gbps"
elif [[ "${media_info}" == *"100baseT"* ]] || [[ "${media_info}" == *"100Base"* ]]; then
link_speed="100 Mbps"
elif [[ "${media_info}" == *"10baseT"* ]] || [[ "${media_info}" == *"10Base"* ]]; then
link_speed="10 Mbps"
elif [[ "${media_info}" == *"10Gbase"* ]]; then
link_speed="10 Gbps"
elif [[ "${media_info}" == *"2500Base"* ]]; then
link_speed="2.5 Gbps"
elif [[ "${media_info}" == *"5000Base"* ]]; then
link_speed="5 Gbps"
else
link_speed="${media_info:-Unknown}"
fi
elif [[ "${hardware_port}" == *"Thunderbolt"* ]]; then
interface_type="Thunderbolt Bridge"
elif [[ "${hardware_port}" == *"Bluetooth"* ]]; then
interface_type="Bluetooth PAN"
else
interface_type="${hardware_port:-Unknown}"
fi
fi
echo "${interface}|${interface_type}|${link_speed}|${hardware_port}"
}
get_system_info() {
log_debug "Gathering system information..."
local model_name
local macos_version
local network_interface
model_name=$(system_profiler SPHardwareDataType 2>/dev/null |
grep "Model Name" | awk -F': ' '{print $2}' | xargs || echo "Unknown")
macos_version=$(sw_vers -productVersion 2>/dev/null || echo "Unknown")
network_interface=$(route get default 2>/dev/null |
grep interface | awk '{print $2}' || echo "Unknown")
cat <<EOF
{
"device_model": "${model_name}",
"macos_version": "${macos_version}",
"network_interface": "${network_interface}",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
}
get_public_ip_info() {
log_debug "Retrieving public IP information from ip-api.com..."
# ip-api.com provides free API with no token required
# It includes more fields like AS, ASN, mobile detection, proxy detection, etc.
local api_url="${IPAPI_BASE}"
local curl_opts=("--silent" "--max-time" "${IPAPI_TIMEOUT}" "--fail")
# Add specific fields we want (comprehensive data)
api_url="${api_url}?fields=status,message,continent,continentCode,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,offset,currency,isp,org,as,asname,reverse,mobile,proxy,hosting,query"
local response
if ! response=$(curl "${curl_opts[@]}" "${api_url}" 2>/dev/null); then
log_warn "Failed to retrieve public IP information"
echo '{"status": "fail", "message": "Failed to retrieve public IP information"}'
return 1
fi
# Check if the API returned success
local status=$(echo "${response}" | jq -r '.status // "fail"' 2>/dev/null)
if [[ "${status}" != "success" ]]; then
log_warn "IP API returned error: $(echo "${response}" | jq -r '.message // "Unknown error"' 2>/dev/null)"
echo "${response}"
return 1
fi
# Transform ip-api.com response to match our expected format
# This ensures compatibility with existing code
echo "${response}" | jq '{
ip: .query,
hostname: .reverse // "",
city: .city,
region: .regionName,
country: .countryCode,
loc: "\(.lat),\(.lon)",
org: .org,
isp: .isp,
as: .as,
asname: .asname,
timezone: .timezone,
mobile: .mobile,
proxy: .proxy,
hosting: .hosting,
postal: .zip // ""
}' 2>/dev/null || echo "${response}"
}
run_network_quality_test() {
local test_number=$1
log_debug "Running network quality test ${test_number}/${TEST_COUNT}..."
local result
# Run networkQuality with verbose output and configuration details
if ! result=$(timeout "${NETWORK_QUALITY_TIMEOUT}" networkQuality -v -c 2>/dev/null); then
log_warn "Network quality test ${test_number} failed or timed out"
echo "ERROR: Test failed or timed out"
return 1
fi
echo "${result}"
}
# =============================================================================
# REPORT GENERATION
# =============================================================================
generate_report_header() {
cat <<EOF
{
"report_metadata": {
"generator": "${SCRIPT_NAME}",
"version": "${SCRIPT_VERSION}",
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"local_time": "$(date '+%B %d, %Y at %I:%M:%S %p')",
"local_time_raw": "$(date)",
"timezone": "$(date +%Z)",
"test_configuration": {
"test_count": ${TEST_COUNT},
"test_interval_seconds": ${TEST_INTERVAL},
"include_system_info": ${INCLUDE_SYSTEM_INFO},
"use_ipv6": ${USE_IPV6}
}
},
EOF
}
# Store test results in memory for report generation
declare -a TEST_RESULTS
declare -a TEST_TIMESTAMPS
declare -a TEST_DURATIONS
run_all_tests() {
# Clear previous results
TEST_RESULTS=()
TEST_TIMESTAMPS=()
TEST_DURATIONS=()
log_info "Running network quality tests..."
for ((i = 1; i <= TEST_COUNT; i++)); do
show_progress "${i}" "${TEST_COUNT}" "Running test ${i}/${TEST_COUNT}..."
local start_time=$(date +%s)
local test_result=$(run_network_quality_test "${i}")
local end_time=$(date +%s)
local duration=$((end_time - start_time))
TEST_RESULTS+=("${test_result}")
TEST_TIMESTAMPS+=("$(date '+%I:%M:%S %p')")
TEST_DURATIONS+=("${duration}")
if [[ ${i} -lt ${TEST_COUNT} && ${TEST_INTERVAL} -gt 0 ]]; then
log_debug "Waiting ${TEST_INTERVAL} seconds before next test..."
sleep "${TEST_INTERVAL}"
fi
done
printf "\r\033[K" >&2 # Clear the progress line
}
generate_whatsapp_report() {
local report_file=$1
# Run all tests first
run_all_tests
# Get system and IP info
local device_model=""
local macos_version=""
local network_interface=""
local network_info=""
local interface_type=""
local link_speed=""
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then
device_model=$(system_profiler SPHardwareDataType 2>/dev/null | grep "Model Name" | awk -F': ' '{print $2}' | xargs || echo "Unknown")
macos_version=$(sw_vers -productVersion 2>/dev/null || echo "Unknown")
network_interface=$(route get default 2>/dev/null | grep interface | awk '{print $2}' || echo "Unknown")
# Get detailed network interface info
network_info=$(get_network_interface_info "${network_interface}")
interface_type=$(echo "${network_info}" | cut -d'|' -f2)
link_speed=$(echo "${network_info}" | cut -d'|' -f3)
fi
# Get DNS servers
local dns_servers=$(get_dns_servers)
# Get IP information
local ip_info=$(get_public_ip_info)
local ip=$(echo "${ip_info}" | jq -r '.ip // "Unknown"' 2>/dev/null)
local isp=$(echo "${ip_info}" | jq -r '.isp // ""' 2>/dev/null)
local org=$(echo "${ip_info}" | jq -r '.org // ""' 2>/dev/null)
local city=$(echo "${ip_info}" | jq -r '.city // "Unknown"' 2>/dev/null)
local country=$(echo "${ip_info}" | jq -r '.country // "Unknown"' 2>/dev/null)
local asn=$(echo "${ip_info}" | jq -r '.as // ""' 2>/dev/null)
local asname=$(echo "${ip_info}" | jq -r '.asname // ""' 2>/dev/null)
log_info "Generating WhatsApp markdown report..."
{
echo "${WA_BOLD_START}πŸ“Š NETWORK QUALITY REPORT${WA_BOLD_END}"
echo ""
echo "${WA_ITALIC_START}$(date '+%B %d, %Y at %I:%M:%S %p')${WA_ITALIC_END}"
echo ""
# Device info
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then
echo "${WA_BOLD_START}DEVICE & NETWORK${WA_BOLD_END}"
echo "β€’ ${device_model} | macOS ${macos_version}"
echo "β€’ Network: ${interface_type} (${network_interface})"
if [[ "${link_speed}" != "Unknown" ]]; then
echo "β€’ Link Speed: ${WA_BOLD_START}${link_speed}${WA_BOLD_END}"
fi
echo ""
fi
# Connection info
echo "${WA_BOLD_START}CONNECTION${WA_BOLD_END}"
echo "β€’ IP: ${WA_BOLD_START}${ip}${WA_BOLD_END}"
echo "β€’ DNS: ${WA_BOLD_START}${dns_servers}${WA_BOLD_END}"
if [[ -n "${isp}" && "${isp}" != "null" ]]; then
echo "β€’ ISP: ${isp}"
elif [[ -n "${org}" && "${org}" != "null" ]]; then
echo "β€’ Provider: ${org}"
fi
echo "β€’ Location: ${city}, ${country}"
if [[ -n "${asn}" && "${asn}" != "null" && "${asn}" != "" ]]; then
if [[ -n "${asname}" && "${asname}" != "null" && "${asname}" != "" ]]; then
echo "β€’ ASN: ${WA_BOLD_START}${asn} ${asname}${WA_BOLD_END}"
else
echo "β€’ ASN: ${WA_BOLD_START}${asn}${WA_BOLD_END}"
fi
fi
echo ""
# Test tool info
echo "${WA_BOLD_START}TESTING TOOL${WA_BOLD_END}"
echo "β€’ ${WA_BOLD_START}networkQuality${WA_BOLD_END} by Apple Inc."
echo "β€’ Built-in macOS 12+ network diagnostic tool"
echo "β€’ Tests real-world network performance"
echo ""
# Test results
echo "${WA_BOLD_START}SPEED TESTS (${TEST_COUNT} tests)${WA_BOLD_END}"
echo ""
local total_dl=0 total_ul=0 total_latency=0
local valid_tests=0
for ((i = 0; i < ${#TEST_RESULTS[@]}; i++)); do
local test_num=$((i + 1))
local result="${TEST_RESULTS[i]}"
if [[ "${result}" != "ERROR"* ]]; then
# Parse the networkQuality JSON output
local dl_throughput=$(echo "${result}" | jq -r '.dl_throughput // 0' 2>/dev/null || echo "0")
local ul_throughput=$(echo "${result}" | jq -r '.ul_throughput // 0' 2>/dev/null || echo "0")
local base_rtt=$(echo "${result}" | jq -r '.base_rtt // 0' 2>/dev/null || echo "0")
local test_endpoint=$(echo "${result}" | jq -r '.test_endpoint // ""' 2>/dev/null)
# Convert to Mbps
local dl_mbps=$(awk "BEGIN {printf \"%.1f\", ${dl_throughput}/1048576}" 2>/dev/null || echo "0")
local ul_mbps=$(awk "BEGIN {printf \"%.1f\", ${ul_throughput}/1048576}" 2>/dev/null || echo "0")
local latency_ms=$(awk "BEGIN {printf \"%.0f\", ${base_rtt}}" 2>/dev/null || echo "0")
echo "${WA_BOLD_START}Test ${test_num}${WA_BOLD_END} (${TEST_TIMESTAMPS[i]})"
if [[ -n "${test_endpoint}" && "${test_endpoint}" != "null" ]]; then
echo "Server: ${WA_ITALIC_START}${test_endpoint}${WA_ITALIC_END}"
fi
echo "↓ ${dl_mbps} Mbps | ↑ ${ul_mbps} Mbps | ${latency_ms}ms"
echo ""
# Add to totals
total_dl=$(awk "BEGIN {print ${total_dl} + ${dl_mbps}}" 2>/dev/null)
total_ul=$(awk "BEGIN {print ${total_ul} + ${ul_mbps}}" 2>/dev/null)
total_latency=$(awk "BEGIN {print ${total_latency} + ${latency_ms}}" 2>/dev/null)
((valid_tests++))
else
echo "${WA_BOLD_START}Test ${test_num}${WA_BOLD_END}: ❌ Failed"
echo ""
fi
done
# Averages
if [[ ${valid_tests} -gt 0 ]]; then
local avg_dl=$(awk "BEGIN {printf \"%.1f\", ${total_dl}/${valid_tests}}" 2>/dev/null)
local avg_ul=$(awk "BEGIN {printf \"%.1f\", ${total_ul}/${valid_tests}}" 2>/dev/null)
local avg_latency=$(awk "BEGIN {printf \"%.0f\", ${total_latency}/${valid_tests}}" 2>/dev/null)
echo "${WA_BOLD_START}AVERAGE${WA_BOLD_END}"
echo "↓ ${WA_BOLD_START}${avg_dl} Mbps${WA_BOLD_END}"
echo "↑ ${WA_BOLD_START}${avg_ul} Mbps${WA_BOLD_END}"
echo "Latency: ${avg_latency}ms"
fi
echo ""
echo "───────────────"
echo "${WA_ITALIC_START}NetScope v${SCRIPT_VERSION}${WA_ITALIC_END}"
echo "${WA_ITALIC_START}Author: Md. Sazzad Hossain Sharkar${WA_ITALIC_END}"
echo "${WA_ITALIC_START}Contact: [email protected]${WA_ITALIC_END}"
} >"${report_file}"
log_info "Report saved to: ${report_file}"
}
generate_text_report() {
local report_file=$1
# Run all tests first
run_all_tests
# Get system info
local device_model=""
local macos_version=""
local network_interface=""
local network_info=""
local interface_type=""
local link_speed=""
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then
device_model=$(system_profiler SPHardwareDataType 2>/dev/null | grep "Model Name" | awk -F': ' '{print $2}' | xargs || echo "Unknown")
macos_version=$(sw_vers -productVersion 2>/dev/null || echo "Unknown")
network_interface=$(route get default 2>/dev/null | grep interface | awk '{print $2}' || echo "Unknown")
# Get detailed network interface info
network_info=$(get_network_interface_info "${network_interface}")
interface_type=$(echo "${network_info}" | cut -d'|' -f2)
link_speed=$(echo "${network_info}" | cut -d'|' -f3)
fi
# Get DNS servers
local dns_servers=$(get_dns_servers)
# Get IP information
local ip_info=$(get_public_ip_info)
local ip=$(echo "${ip_info}" | jq -r '.ip // "Unknown"' 2>/dev/null)
local isp=$(echo "${ip_info}" | jq -r '.isp // ""' 2>/dev/null)
local org=$(echo "${ip_info}" | jq -r '.org // ""' 2>/dev/null)
local city=$(echo "${ip_info}" | jq -r '.city // "Unknown"' 2>/dev/null)
local country=$(echo "${ip_info}" | jq -r '.country // "Unknown"' 2>/dev/null)
local asn=$(echo "${ip_info}" | jq -r '.as // ""' 2>/dev/null)
log_info "Generating text report..."
{
echo "NETWORK QUALITY REPORT"
echo "======================"
echo ""
echo "Date: $(date '+%B %d, %Y at %I:%M:%S %p')"
echo "Tests Performed: ${TEST_COUNT}"
echo ""
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then
echo "DEVICE INFORMATION"
echo "------------------"
echo "Device: ${device_model}"
echo "OS: macOS ${macos_version}"
echo "Network Interface: ${network_interface} (${interface_type})"
if [[ "${link_speed}" != "Unknown" ]]; then
echo "Link Speed: ${link_speed}"
fi
echo ""
fi
echo "NETWORK INFORMATION"
echo "-------------------"
echo "Public IP: ${ip}"
if [[ -n "${isp}" && "${isp}" != "null" ]]; then
echo "ISP: ${isp}"
elif [[ -n "${org}" && "${org}" != "null" ]]; then
echo "Provider: ${org}"
fi
echo "Location: ${city}, ${country}"
if [[ -n "${asn}" && "${asn}" != "null" && "${asn}" != "" ]]; then
echo "ASN: ${asn}"
fi
echo ""
echo "TESTING TOOL"
echo "------------"
echo "Tool: Apple networkQuality"
echo "Type: Built-in macOS 12+ network diagnostic utility"
echo "Method: Real-world performance testing with parallel connections"
echo "Metrics: Download/Upload throughput, Latency, and Responsiveness"
echo ""
echo "SPEED TEST RESULTS"
echo "------------------"
local total_dl=0 total_ul=0 total_latency=0
local valid_tests=0
for ((i = 0; i < ${#TEST_RESULTS[@]}; i++)); do
local test_num=$((i + 1))
local result="${TEST_RESULTS[i]}"
echo "Test #${test_num} (${TEST_TIMESTAMPS[i]}, Duration: ${TEST_DURATIONS[i]}s)"
if [[ "${result}" != "ERROR"* ]]; then
# Parse the networkQuality JSON output
local dl_throughput=$(echo "${result}" | jq -r '.dl_throughput // 0' 2>/dev/null || echo "0")
local ul_throughput=$(echo "${result}" | jq -r '.ul_throughput // 0' 2>/dev/null || echo "0")
local base_rtt=$(echo "${result}" | jq -r '.base_rtt // 0' 2>/dev/null || echo "0")
local responsiveness=$(echo "${result}" | jq -r '.responsiveness // 0' 2>/dev/null || echo "0")
local test_endpoint=$(echo "${result}" | jq -r '.test_endpoint // ""' 2>/dev/null)
# Convert to Mbps
local dl_mbps=$(awk "BEGIN {printf \"%.2f\", ${dl_throughput}/1048576}" 2>/dev/null || echo "0")
local ul_mbps=$(awk "BEGIN {printf \"%.2f\", ${ul_throughput}/1048576}" 2>/dev/null || echo "0")
local latency_ms=$(awk "BEGIN {printf \"%.1f\", ${base_rtt}}" 2>/dev/null || echo "0")
local resp_rpm=$(awk "BEGIN {printf \"%.0f\", ${responsiveness}}" 2>/dev/null || echo "0")
if [[ -n "${test_endpoint}" && "${test_endpoint}" != "null" ]]; then
echo " Test Server: ${test_endpoint}"
fi
echo " Download: ${dl_mbps} Mbps"
echo " Upload: ${ul_mbps} Mbps"
echo " Latency: ${latency_ms} ms"
echo " Responsiveness: ${resp_rpm} RPM"
# Add to totals
total_dl=$(awk "BEGIN {print ${total_dl} + ${dl_mbps}}" 2>/dev/null)
total_ul=$(awk "BEGIN {print ${total_ul} + ${ul_mbps}}" 2>/dev/null)
total_latency=$(awk "BEGIN {print ${total_latency} + ${latency_ms}}" 2>/dev/null)
((valid_tests++))
else
echo " Status: Test Failed"
fi
echo ""
done
if [[ ${valid_tests} -gt 0 ]]; then
echo "AVERAGE PERFORMANCE"
echo "-------------------"
local avg_dl=$(awk "BEGIN {printf \"%.2f\", ${total_dl}/${valid_tests}}" 2>/dev/null)
local avg_ul=$(awk "BEGIN {printf \"%.2f\", ${total_ul}/${valid_tests}}" 2>/dev/null)
local avg_latency=$(awk "BEGIN {printf \"%.1f\", ${total_latency}/${valid_tests}}" 2>/dev/null)
echo "Average Download: ${avg_dl} Mbps"
echo "Average Upload: ${avg_ul} Mbps"
echo "Average Latency: ${avg_latency} ms"
echo ""
fi
echo "======================"
echo "Report Generated by NetScope v${SCRIPT_VERSION}"
echo "Author: Md. Sazzad Hossain Sharkar ([email protected])"
} >"${report_file}"
log_info "Text report saved to: ${report_file}"
}
# =============================================================================
# COMMAND LINE INTERFACE
# =============================================================================
show_version() {
echo "${SCRIPT_NAME} version ${SCRIPT_VERSION}"
exit 0
}
show_help() {
cat <<EOF
${BOLD}NetScope${NC} - Professional Network Quality Analysis Tool
${CYAN}Version ${SCRIPT_VERSION}${NC}
${BOLD}QUICK START${NC}
${GREEN}${SCRIPT_NAME}${NC} # Run with defaults (3 tests, 30s interval)
${GREEN}${SCRIPT_NAME} -f whatsapp${NC} # Generate WhatsApp-ready report
${GREEN}${SCRIPT_NAME} -c 1 -i 10${NC} # Quick single test
${BOLD}DESCRIPTION${NC}
Generates comprehensive network performance reports perfect for ISP support.
Tests your connection quality using Apple's networkQuality tool and provides
detailed diagnostics in multiple formats.
${CYAN}Reports are saved to: ~/Desktop/network-quality-reports/${NC}
${BOLD}COMMON USE CASES${NC}
${YELLOW}1. Quick Network Check:${NC}
${GREEN}${SCRIPT_NAME} -c 1 -i 10${NC}
β†’ Single test with minimal wait time
${YELLOW}2. Detailed ISP Report (Recommended):${NC}
${GREEN}${SCRIPT_NAME} -c 5 -i 60 -f text${NC}
β†’ 5 tests over 5 minutes for reliable data
${YELLOW}3. Share via WhatsApp/Social Media:${NC}
${GREEN}${SCRIPT_NAME} -f whatsapp${NC}
β†’ Formatted with emojis for easy mobile sharing
${BOLD}OUTPUT FORMATS${NC}
${CYAN}whatsapp${NC} Mobile-friendly with emojis for messaging apps (default)
${CYAN}text${NC} Human-readable report for ISP technicians
${BOLD}OPTIONS${NC}
${BOLD}Basic:${NC}
-h, --help Show this help message
-v, --version Show version information
${BOLD}Test Settings:${NC}
-c, --count N Number of tests (1-${MAX_TEST_COUNT}, default: ${DEFAULT_TEST_COUNT})
-i, --interval SECS Seconds between tests (min: ${MIN_TEST_INTERVAL}, default: ${DEFAULT_TEST_INTERVAL})
${BOLD}Output Control:${NC}
-f, --format FORMAT Output format: whatsapp, text (default: whatsapp)
-o, --output FILE Custom output path (default: auto-generated)
--no-system-info Skip device information
${BOLD}Advanced:${NC}
# -t, --token TOKEN (Deprecated - ip-api.com doesn't need tokens)
--ipv6 Test IPv6 connectivity
--verbose Show detailed progress
--debug Enable debug output
${BOLD}Configuration:${NC}
--save-config Save current settings as defaults
--show-config Display saved configuration
${BOLD}IP GEOLOCATION SERVICE${NC}
This tool uses ${CYAN}ip-api.com${NC} for free IP geolocation data:
β€’ No API token required
β€’ Includes ISP, ASN, and network details
β€’ Mobile/Proxy detection
β€’ 45 requests per minute limit (more than enough)
The service automatically provides comprehensive network information
without any configuration needed.
${BOLD}EXAMPLES${NC}
${YELLOW}Basic report (no configuration needed):${NC}
${GREEN}${SCRIPT_NAME}${NC}
${YELLOW}ISP troubleshooting report:${NC}
${GREEN}${SCRIPT_NAME} -c 5 -i 60 -f text${NC}
β†’ Creates detailed report with 5 tests over 5 minutes
${YELLOW}Quick test for immediate results:${NC}
${GREEN}${SCRIPT_NAME} -c 1 -i 10 -f whatsapp${NC}
β†’ Single test, WhatsApp format, ready in ~20 seconds
${YELLOW}Continuous monitoring (maximum):${NC}
${GREEN}${SCRIPT_NAME} -c 10 -i 120 --verbose${NC}
β†’ 10 tests over 20 minutes with live progress
${YELLOW}Custom location with text output:${NC}
${GREEN}${SCRIPT_NAME} -f text -o ~/Documents/network_test.txt${NC}
${YELLOW}One-time API token setup:${NC}
${GREEN}${SCRIPT_NAME} -t abc123def456 --save-config${NC}
β†’ Token saved for all future reports
${BOLD}OUTPUT FILES${NC}
Reports are saved with timestamps:
β€’ ${CYAN}network_report_2025-01-19_03-45-00_PM.txt${NC}
Default location: ${CYAN}~/Desktop/network-quality-reports/${NC}
${BOLD}TROUBLESHOOTING${NC}
${YELLOW}If tests fail:${NC}
β€’ Ensure you're on macOS 12.0 or later
β€’ Check internet connection stability
β€’ Try with fewer tests: -c 1
${YELLOW}For slow networks:${NC}
β€’ Increase interval: -i 120 (2 minutes between tests)
β€’ Reduce test count: -c 3
${BOLD}AUTHOR${NC}
Md. Sazzad Hossain Sharkar
Email: ${CYAN}[email protected]${NC}
${BOLD}SUPPORT${NC}
For issues, feature requests, or contributions:
Email: ${CYAN}[email protected]${NC}
${BOLD}SEE ALSO${NC}
networkQuality(1) - Apple's network testing tool
${CYAN}https://ip-api.com${NC} - Free IP geolocation service
EOF
exit 0
}
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-h | --help)
show_help
;;
-v | --version)
show_version
;;
--verbose)
VERBOSE=true
shift
;;
--debug)
DEBUG=true
VERBOSE=true
shift
;;
-c | --count)
if [[ -z "${2:-}" ]] || [[ ! "${2}" =~ ^[0-9]+$ ]] || [[ "${2}" -lt 1 ]] || [[ "${2}" -gt ${MAX_TEST_COUNT} ]]; then
log_fatal "Invalid test count. Must be 1-${MAX_TEST_COUNT}"
fi
TEST_COUNT="$2"
shift 2
;;
-i | --interval)
if [[ -z "${2:-}" ]] || [[ ! "${2}" =~ ^[0-9]+$ ]] || [[ "${2}" -lt ${MIN_TEST_INTERVAL} ]]; then
log_fatal "Invalid interval. Must be β‰₯${MIN_TEST_INTERVAL} seconds"
fi
TEST_INTERVAL="$2"
shift 2
;;
-o | --output)
if [[ -z "${2:-}" ]]; then
log_fatal "Output file cannot be empty"
fi
OUTPUT_FILE="$2"
shift 2
;;
-f | --format)
if [[ "${2:-}" =~ ^(text|whatsapp)$ ]]; then
OUTPUT_FORMAT="$2"
else
log_fatal "Invalid format. Must be 'text' or 'whatsapp'"
fi
shift 2
;;
-t | --token)
log_warn "API tokens are no longer needed. Using free ip-api.com service."
shift 2
;;
--ipv6)
USE_IPV6=true
shift
;;
--no-system-info)
INCLUDE_SYSTEM_INFO=false
shift
;;
--save-config)
save_config
log_info "Configuration saved to: ${CONFIG_FILE}"
exit 0
;;
--show-config)
if [[ -f "${CONFIG_FILE}" ]]; then
cat "${CONFIG_FILE}"
else
log_info "No configuration file found at: ${CONFIG_FILE}"
fi
exit 0
;;
-*)
log_fatal "Unknown option: $1"
;;
*)
log_fatal "Unexpected argument: $1"
;;
esac
done
}
# =============================================================================
# MAIN EXECUTION
# =============================================================================
main() {
# Initialize
create_directories
load_config
parse_arguments "$@"
check_dependencies
# Validate jq is available for API response processing
if ! command -v jq >/dev/null 2>&1; then
log_fatal "jq is required. Please install with: brew install jq"
fi
# Determine output file
if [[ -z "${OUTPUT_FILE}" ]]; then
local ext="txt"
[[ "${OUTPUT_FORMAT}" == "whatsapp" ]] && ext="txt"
OUTPUT_FILE="${LOG_DIR}/network_report_${TIMESTAMP}.${ext}"
fi
# Ensure output directory exists
local output_dir
output_dir="$(dirname "${OUTPUT_FILE}")"
if [[ ! -d "${output_dir}" ]]; then
mkdir -p "${output_dir}" || log_fatal "Cannot create output directory: ${output_dir}"
fi
# Display configuration if verbose
if [[ "${VERBOSE}" == true ]]; then
log_info "Configuration:"
log_info " Test count: ${TEST_COUNT}"
log_info " Test interval: ${TEST_INTERVAL} seconds"
log_info " Output format: ${OUTPUT_FORMAT}"
log_info " Output file: ${OUTPUT_FILE}"
log_info " Include system info: ${INCLUDE_SYSTEM_INFO}"
log_info " Use IPv6: ${USE_IPV6}"
log_info " IP API: ip-api.com (no token required)"
echo ""
fi
# Generate report
case "${OUTPUT_FORMAT}" in
whatsapp)
generate_whatsapp_report "${OUTPUT_FILE}"
;;
text)
generate_text_report "${OUTPUT_FILE}"
;;
esac
log_info "${GREEN}Report generation completed successfully!${NC}"
if [[ "${VERBOSE}" == true ]]; then
echo ""
log_info "Next steps:"
log_info " 1. Review the generated report"
log_info " 2. Share with your ISP for network debugging"
log_info " 3. Run additional tests if needed"
fi
}
# =============================================================================
# SCRIPT EXECUTION
# =============================================================================
# Trap signals for cleanup
trap 'log_error "Script interrupted"; exit 130' INT TERM
# Run main function
main "$@"

NetScope - Network Quality Analysis Tool for macOS

Overview

NetScope is a professional-grade network quality analysis tool designed for macOS users and ISP technicians. It provides comprehensive network performance reports using Apple's built-in networkQuality tool, perfect for troubleshooting connectivity issues.

Features

  • πŸš€ Multiple Test Runs: Configure 1-10 sequential tests with custom intervals
  • πŸ“Š Dual Output Formats: WhatsApp-ready markdown or detailed plain text reports
  • 🌍 IP Geolocation: Automatic ISP, location, and ASN detection via ip-api.com
  • πŸ’» System Information: Device model, macOS version, network interface details
  • πŸ”Œ Network Detection: Identifies Ethernet/Wi-Fi connection type and link speed
  • 🎯 DNS Configuration: Shows active DNS servers (excluding local resolvers)
  • ⚑ Real-time Progress: Visual feedback during testing
  • πŸ“ Desktop Export: Reports saved to ~/Desktop/network-quality-reports/

Requirements

  • macOS 12.0 (Monterey) or later
  • networkQuality tool (built-in on macOS 12+)
  • jq for JSON parsing (brew install jq)
  • Internet connection

Installation

Quick Install (One-liner)

curl -sSL https://gist.githubusercontent.com/SHSharkar/6a6eba963c2f0b7bcdea9d6c478c9a49/raw/netscope -o ~/bin/netscope && chmod +x ~/bin/netscope

Manual Installation

  1. Create bin directory (if it doesn't exist):
mkdir -p ~/bin
  1. Download the script:
curl -o ~/bin/netscope https://gist.githubusercontent.com/SHSharkar/6a6eba963c2f0b7bcdea9d6c478c9a49/raw/netscope
  1. Make it executable:
chmod +x ~/bin/netscope
  1. Add ~/bin to PATH (if not already added):

For zsh (default on modern macOS):

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

For bash:

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
  1. Install jq (if not installed):
brew install jq

Usage

Basic Usage

# Run with defaults (3 tests, 30s interval, WhatsApp format)
netscope

# Quick single test
netscope -c 1

# Detailed ISP troubleshooting (5 tests over 5 minutes)
netscope -c 5 -i 60 -f text

Command Options

-h, --help              Show help message
-v, --version           Show version information
-c, --count N           Number of tests (1-10, default: 3)
-i, --interval SECS     Seconds between tests (min: 10, default: 30)
-f, --format FORMAT     Output format: whatsapp, text (default: whatsapp)
-o, --output FILE       Custom output path
--no-system-info        Skip device information
--verbose               Show detailed progress
--debug                 Enable debug output

Examples

Quick network check:

netscope -c 1 -i 10

ISP support report:

netscope -c 5 -i 60 -f text

WhatsApp sharing:

netscope -f whatsapp
# Copy the generated report from Desktop and paste in WhatsApp

Custom output location:

netscope -o ~/Documents/network_test.txt

Output Formats

WhatsApp Format

  • Mobile-friendly with emojis
  • Bold/italic markdown formatting
  • Optimized for messaging apps
  • Perfect for quick sharing

Text Format

  • Detailed technical report
  • Comprehensive metrics
  • ISP technician friendly
  • Professional documentation

Report Contents

  • Device Info: Mac model, macOS version
  • Network Details: Interface type (Ethernet/Wi-Fi), link speed
  • Connection Info: Public IP, DNS servers, ISP, location, ASN
  • Test Results: Download/upload speeds, latency, server endpoints
  • Statistics: Average performance across all tests

Troubleshooting

"networkQuality command not available"

  • Requires macOS 12.0 or later
  • Update your macOS to Monterey or newer

"jq: command not found"

"netscope: command not found"

  • Ensure ~/bin is in your PATH
  • Run: echo $PATH to verify
  • Restart terminal after PATH modification

Tests failing or timing out

  • Check internet connection stability
  • Try with fewer tests: netscope -c 1
  • Increase timeout interval: netscope -i 120

API Information

NetScope uses ip-api.com for IP geolocation:

  • Free service, no API key required
  • Provides ISP, ASN, location data
  • Rate limit: 45 requests/minute
  • More info: https://ip-api.com

Author

Md. Sazzad Hossain Sharkar
Email: [email protected]

License

MIT License - Feel free to use and modify

Version

Current version: 1.2.0

Support

For issues or feature requests, contact: [email protected]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment