Skip to content

Instantly share code, notes, and snippets.

@nedzen
Created June 21, 2025 18:22
Show Gist options
  • Save nedzen/0fcfdf0198bb78eadb6a8abdcb7e4efe to your computer and use it in GitHub Desktop.
Save nedzen/0fcfdf0198bb78eadb6a8abdcb7e4efe to your computer and use it in GitHub Desktop.
Domain checker
#!/opt/homebrew/bin/bash
# domain.sh - Elegant Domain Availability Checker
#
# A simplified, robust domain availability checker with parallel processing,
# retry logic, progressive logging, and elegant table output with emoji status indicators.
#
# Usage:
# ./domain.sh data.json -d com,net,org,io,es,co.uk,info
#
# Features:
# - Parallel domain checking with configurable workers (default: 3)
# - Automatic retry logic for failed queries
# - Progressive logging - saves results as it goes for safe interruption
# - Table output with extensions as columns and emoji status
# - Resume capability - continues from where it left off
# - Colored terminal output with progress tracking (uses pygmentize if available)
# - Single file solution with no external config needed
#
# Requirements:
# - bash 4.0+, jq, whois, timeout/gtimeout
# - pygmentize (optional, for enhanced color output)
#
# Output:
# - Terminal: Colored table with real-time progress
# - Log file: {input_filename}.log for resume capability
#
# Status Indicators:
# - βœ… Available for registration
# - β­• Already taken/registered
# - ❓ Error or timeout
# - ⏳ Currently checking
#
# Author: Simplified Domain Checker
# Version: 3.0 (Enhanced Debug)
# Check for help flag FIRST, before any other processing
for arg in "$@"; do
if [[ "$arg" == "-h" || "$arg" == "--help" ]]; then
cat << 'EOF'
Domain Availability Checker v3.0 - Elegant Domain Availability Checker
Usage:
domain.sh data.json -d ext1[,ext2,...]
Arguments:
data.json JSON file containing an array of domain names
-d, --domains Comma-separated list of extensions (required)
Options:
-w, --workers NUM Number of parallel workers (default: 3)
-r, --rate-limit SEC Delay between queries (default: 0.5)
-t, --timeout SEC WHOIS timeout (default: 10)
--retries NUM Retry attempts on failure (default: 2)
-v, --verbose Enable verbose (debug) output
-q, --quiet Suppress progress and informational output
-h, --help Show this help message
Examples:
./domain.sh domains.json -d com,net,org
./domain.sh domains.json -d com,net,io,co.uk -w 5
./domain.sh domains.json -d com,es,info --verbose
Status Indicators:
βœ… Available β­• Taken ❓ Error/Timeout
EOF
exit 0
fi
done
set -euo pipefail
# --- Configuration ---
readonly SCRIPT_NAME=$(basename "$0")
readonly VERSION="3.0"
readonly DEFAULT_WORKERS=3
readonly DEFAULT_RATE_LIMIT=0.5
readonly DEFAULT_TIMEOUT=10
readonly DEFAULT_RETRIES=2
# --- Global variables ---
WORKERS=$DEFAULT_WORKERS
RATE_LIMIT=$DEFAULT_RATE_LIMIT
TIMEOUT=$DEFAULT_TIMEOUT
RETRIES=$DEFAULT_RETRIES
DATA_FILE=""
EXTENSIONS=()
LOGFILE=""
TEMP_DIR=""
QUIET=false
VERBOSE=false
HAS_PYGMENTIZE=false
# --- Counters ---
TOTAL_DOMAINS=0
JOBS_TO_PROCESS=0
# --- Colors and Emojis ---
readonly GREEN='\033[0;32m'
readonly RED='\033[0;31m'
readonly BLUE='\033[0;34m'
readonly YELLOW='\033[0;33m'
readonly CYAN='\033[0;36m'
readonly BOLD='\033[1m'
readonly RESET='\033[0m'
readonly EMOJI_AVAILABLE="βœ…"
readonly EMOJI_TAKEN="β­•"
readonly EMOJI_ERROR="❓"
readonly EMOJI_CHECKING="⏳"
# --- Functions ---
# Provides a generic WHOIS pattern to detect an available domain.
get_whois_pattern() {
echo "No match|NOT FOUND|Status: free|No Data Found|No entries found|Not Registered|is free|domain name not known|Status: AVAILABLE|Not found|Domain not found|is available for registration|does not exist|libre|This domain name has not been registered"
}
# Print a colored message to the terminal. Uses basic ANSI colors.
print_color() {
local color=$1
local message=$2
if [[ "$QUIET" != "true" ]]; then
echo -e "${color}${message}${RESET}"
fi
}
# Log a message if verbose mode is enabled
log_verbose() {
if [[ "$VERBOSE" == "true" ]]; then
print_color "$BLUE" "[DEBUG] $1" >&2
fi
}
# Print an error message and exit
error_exit() {
print_color "$RED" "❌ Error: $1" >&2
exit 1
}
# Check for required command-line dependencies
check_dependencies() {
log_verbose "Checking dependencies..."
local required_deps=("jq" "whois")
for cmd in "${required_deps[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
error_exit "'$cmd' is required but not installed."
fi
log_verbose "Found: $cmd"
done
# Check for timeout command (GNU coreutils or built-in)
if command -v timeout &> /dev/null; then
log_verbose "Found: timeout"
elif command -v gtimeout &> /dev/null; then
log_verbose "Found: gtimeout"
else
error_exit "'timeout' command is required. On macOS, install with: brew install coreutils"
fi
# Check for pygmentize (optional dependency for better colors)
if command -v pygmentize &> /dev/null; then
HAS_PYGMENTIZE=true
log_verbose "Found: pygmentize (optional)"
fi
log_verbose "All dependencies satisfied"
}
# Get the correct timeout command ('timeout' or 'gtimeout')
get_timeout_cmd() {
if command -v timeout &> /dev/null; then
echo "timeout"
elif command -v gtimeout &> /dev/null; then
echo "gtimeout"
else
error_exit "Neither 'timeout' nor 'gtimeout' could be found."
fi
}
# Check if a domain result already exists (bash 4.x compatible)
result_exists() {
local domain=$1
local ext=$2
if [[ -f "$LOGFILE.progress" ]]; then
if grep -q "^$domain,$ext," "$LOGFILE.progress" 2>/dev/null; then
log_verbose "Result exists for $domain.$ext"
return 0
fi
fi
log_verbose "No existing result for $domain.$ext"
return 1
}
# Check a single domain's availability with retry logic
check_domain_availability() {
local domain=$1
local ext=$2
local full_domain="${domain}.${ext}"
local attempt=1
local timeout_cmd
timeout_cmd=$(get_timeout_cmd)
log_verbose "Checking $full_domain..."
while [[ $attempt -le $RETRIES ]]; do
local whois_output=""
local pattern
pattern=$(get_whois_pattern)
# Perform WHOIS query with timeout
if whois_output=$($timeout_cmd "$TIMEOUT" whois "$full_domain" 2>/dev/null); then
if [[ -n "$whois_output" ]]; then
if echo "$whois_output" | grep -qiE "$pattern"; then
echo "available"
return 0
else
echo "taken"
return 0
fi
fi
fi
log_verbose "WHOIS query failed for $full_domain (attempt $attempt/$RETRIES)."
((attempt++))
[[ $attempt -le $RETRIES ]] && sleep 1
done
log_verbose "All retry attempts failed for $full_domain."
echo "error"
return 1
}
# A worker process that pulls jobs from a queue file
worker_process() {
local worker_id=$1
log_verbose "Worker $worker_id started"
while true; do
local job=""
local lock_file="$TEMP_DIR/queue.lock"
# Simple file-based queue with basic locking
local timeout=0
while [[ $timeout -lt 10 ]]; do
if (set -C; echo $$ > "$lock_file") 2>/dev/null; then
job=$(head -n 1 "$TEMP_DIR/queue" 2>/dev/null || true)
if [[ -n "$job" ]]; then
tail -n +2 "$TEMP_DIR/queue" > "$TEMP_DIR/queue.tmp" 2>/dev/null || true
mv "$TEMP_DIR/queue.tmp" "$TEMP_DIR/queue" 2>/dev/null || true
fi
rm -f "$lock_file"
break
else
sleep 0.1
((timeout++))
fi
done
[[ -z "$job" ]] && break
# Parse job: domain,extension
IFS=',' read -r domain ext <<< "$job"
# Rate limiting
sleep "$RATE_LIMIT"
# Check domain
local status=$(check_domain_availability "$domain" "$ext")
# Save result immediately to progress file for progressive logging
echo "$domain,$ext,$status" >> "$LOGFILE.progress"
# Update progress counter
local completed_file="$TEMP_DIR/completed"
local completed=$(cat "$completed_file" 2>/dev/null || echo 0)
echo $((completed + 1)) > "$completed_file"
log_verbose "Completed: $domain.$ext = $status"
done
log_verbose "Worker $worker_id finished"
}
# Parse command-line arguments
parse_args() {
log_verbose "Parsing arguments: $*"
# Handle JSON file as first argument if provided
if [[ $# -gt 0 && -f "$1" && "$1" == *.json ]]; then
DATA_FILE="$1"
log_verbose "Found JSON file: $DATA_FILE"
shift
fi
while [[ $# -gt 0 ]]; do
case $1 in
-d|--domains)
[[ $# -lt 2 ]] && error_exit "Option $1 requires an argument"
shift
IFS=',' read -r -a EXTENSIONS <<< "$1"
log_verbose "Extensions: ${EXTENSIONS[*]}"
;;
-w|--workers)
[[ $# -lt 2 ]] && error_exit "Option $1 requires an argument"
shift
WORKERS=$1
[[ ! "$WORKERS" =~ ^[0-9]+$ ]] && error_exit "Invalid worker count: $WORKERS"
[[ $WORKERS -lt 1 || $WORKERS -gt 20 ]] && error_exit "Workers must be between 1 and 20"
log_verbose "Workers: $WORKERS"
;;
-r|--rate-limit)
[[ $# -lt 2 ]] && error_exit "Option $1 requires an argument"
shift
RATE_LIMIT=$1
[[ ! "$RATE_LIMIT" =~ ^[0-9]*\.?[0-9]+$ ]] && error_exit "Invalid rate limit: $RATE_LIMIT"
log_verbose "Rate limit: $RATE_LIMIT"
;;
-t|--timeout)
[[ $# -lt 2 ]] && error_exit "Option $1 requires an argument"
shift
TIMEOUT=$1
[[ ! "$TIMEOUT" =~ ^[0-9]+$ ]] && error_exit "Invalid timeout: $TIMEOUT"
log_verbose "Timeout: $TIMEOUT"
;;
--retries)
[[ $# -lt 2 ]] && error_exit "Option $1 requires an argument"
shift
RETRIES=$1
[[ ! "$RETRIES" =~ ^[0-9]+$ ]] && error_exit "Invalid retry count: $RETRIES"
log_verbose "Retries: $RETRIES"
;;
-v|--verbose)
VERBOSE=true
log_verbose "Verbose mode enabled"
;;
-q|--quiet)
QUIET=true
;;
*)
if [[ -z "$DATA_FILE" && -f "$1" && "$1" == *.json ]]; then
DATA_FILE="$1"
log_verbose "Found JSON file: $DATA_FILE"
else
error_exit "Unknown parameter: $1"
fi
;;
esac
shift
done
# Validate required arguments
[[ -z "$DATA_FILE" ]] && error_exit "No JSON data file specified. Use: $SCRIPT_NAME file.json -d extensions"
[[ ! -f "$DATA_FILE" ]] && error_exit "Data file '$DATA_FILE' not found."
[[ ${#EXTENSIONS[@]} -eq 0 ]] && error_exit "No domain extensions specified. Use -d com,net,..."
log_verbose "Validating JSON file: $DATA_FILE"
# Validate JSON file
if ! jq empty "$DATA_FILE" 2>/dev/null; then
error_exit "Invalid JSON in file: $DATA_FILE"
fi
# Check if JSON contains an array
if ! jq -e 'type == "array"' "$DATA_FILE" >/dev/null 2>&1; then
error_exit "JSON file must contain an array of domain names"
fi
LOGFILE="${DATA_FILE%.json}.log"
log_verbose "Log file will be: $LOGFILE"
}
# Load existing results count
count_existing_results() {
local count=0
if [[ -f "$LOGFILE.progress" ]]; then
count=$(wc -l < "$LOGFILE.progress" 2>/dev/null | tr -d ' ')
if [[ $count -gt 0 ]]; then
print_color "$YELLOW" "πŸ“‹ Resuming session. Found $count existing results."
fi
fi
log_verbose "Existing results count: $count"
echo $count
}
# Build the work queue, excluding domains that have already been checked
build_work_queue() {
log_verbose "Building work queue..."
local domains=()
readarray -t domains < <(jq -r '.[]' "$DATA_FILE" 2>/dev/null)
[[ ${#domains[@]} -eq 0 ]] && error_exit "No domains found in JSON file"
TOTAL_DOMAINS=${#domains[@]}
log_verbose "Found $TOTAL_DOMAINS domains in JSON file"
local queue_file="$TEMP_DIR/queue"
> "$queue_file"
echo "0" > "$TEMP_DIR/completed"
local total_jobs=0
log_verbose "Checking ${#EXTENSIONS[@]} extensions: ${EXTENSIONS[*]}"
for domain in "${domains[@]}"; do
[[ -z "$domain" ]] && continue
log_verbose "Processing domain: '$domain'"
for ext in "${EXTENSIONS[@]}"; do
[[ -z "$ext" ]] && continue
log_verbose " Checking extension: '$ext'"
if ! result_exists "$domain" "$ext"; then
echo "$domain,$ext" >> "$queue_file"
((total_jobs++))
log_verbose " Added job: $domain.$ext"
else
log_verbose " Skipped existing: $domain.$ext"
fi
done
done
log_verbose "Built queue with $total_jobs jobs"
echo $total_jobs
}
# Display progress
show_progress() {
local initial_jobs=$(wc -l < "$TEMP_DIR/queue" 2>/dev/null || echo 0)
while true; do
local completed=$(cat "$TEMP_DIR/completed" 2>/dev/null || echo 0)
local current_jobs=$(wc -l < "$TEMP_DIR/queue" 2>/dev/null || echo 0)
[[ $current_jobs -le 0 ]] && break
local percentage=0
if [[ $initial_jobs -gt 0 ]]; then
percentage=$(( (initial_jobs - current_jobs) * 100 / initial_jobs ))
fi
printf "\r${CYAN}⏳ Progress: %d/%d (%d%%) - %d remaining${RESET}" \
"$((initial_jobs - current_jobs))" "$initial_jobs" "$percentage" "$current_jobs"
sleep 2
done
printf "\r%-80s\r" "" # Clear progress line
}
# Get result for a domain.ext combination
get_result() {
local domain=$1
local ext=$2
if [[ -f "$LOGFILE.progress" ]]; then
grep "^$domain,$ext," "$LOGFILE.progress" 2>/dev/null | cut -d',' -f3 | tail -1
fi
}
# Generate and display the final results table
generate_table_output() {
log_verbose "Generating results table..."
local domains=()
readarray -t domains < <(jq -r '.[]' "$DATA_FILE" 2>/dev/null)
[[ ${#domains[@]} -eq 0 ]] && return
# Calculate column widths
local max_domain_width=6
for domain in "${domains[@]}"; do
[[ ${#domain} -gt $max_domain_width ]] && max_domain_width=${#domain}
done
local ext_col_width=8
for ext in "${EXTENSIONS[@]}"; do
local needed_width=$(( ${#ext} + 4 ))
[[ $needed_width -gt $ext_col_width ]] && ext_col_width=$needed_width
done
# Build table
local header=$(printf "%-${max_domain_width}s" "Domain")
local separator=$(printf "%-${max_domain_width}s" "" | tr ' ' '-')
for ext in "${EXTENSIONS[@]}"; do
header+=" | $(printf "%-${ext_col_width}s" ".${ext}")"
separator+="---$(printf "%-${ext_col_width}s" "" | tr ' ' '-')"
done
# Generate output
{
echo
echo "πŸ“Š Domain Availability Results"
echo
echo "$header"
echo "$separator"
for domain in "${domains[@]}"; do
local row=$(printf "%-${max_domain_width}s" "$domain")
for ext in "${EXTENSIONS[@]}"; do
local status=$(get_result "$domain" "$ext")
local cell_content="$EMOJI_CHECKING"
case "$status" in
"available") cell_content="$EMOJI_AVAILABLE" ;;
"taken") cell_content="$EMOJI_TAKEN" ;;
"error") cell_content="$EMOJI_ERROR" ;;
esac
row+=" | $(printf "%*s" $(( (ext_col_width + 2) / 2 )) "$cell_content")"
done
echo "$row"
done
echo
} > "$LOGFILE"
if [[ "$QUIET" != "true" ]]; then
cat "$LOGFILE"
fi
# Summary
local available_count taken_count error_count
available_count=$(grep -o "$EMOJI_AVAILABLE" "$LOGFILE" 2>/dev/null | wc -l | tr -d ' ')
taken_count=$(grep -o "$EMOJI_TAKEN" "$LOGFILE" 2>/dev/null | wc -l | tr -d ' ')
error_count=$(grep -o "$EMOJI_ERROR" "$LOGFILE" 2>/dev/null | wc -l | tr -d ' ')
print_color "$GREEN" "$EMOJI_AVAILABLE Available: $available_count"
print_color "$RED" "$EMOJI_TAKEN Taken: $taken_count"
[[ $error_count -gt 0 ]] && print_color "$YELLOW" "$EMOJI_ERROR Errors: $error_count"
print_color "$BLUE" "πŸ“„ Results saved to: $LOGFILE"
}
# Cleanup function
cleanup() {
trap - EXIT INT TERM
local exit_code=$?
log_verbose "Cleanup called with exit code: $exit_code"
# Kill child processes
if command -v pkill >/dev/null 2>&1; then
pkill -P $$ 2>/dev/null || true
fi
if [[ $exit_code -eq 130 ]]; then
echo
print_color "$YELLOW" "⚠️ Process interrupted by user."
fi
# Show resume info if progress was made
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" && -f "$LOGFILE.progress" ]]; then
local progress_count
progress_count=$(wc -l < "$LOGFILE.progress" 2>/dev/null | tr -d ' ')
if [[ $progress_count -gt 0 ]]; then
generate_table_output 2>/dev/null || true
local extensions_formatted
extensions_formatted=$(IFS=','; echo "${EXTENSIONS[*]}")
echo
print_color "$CYAN" "πŸ’Ύ Progress saved. $progress_count checks completed."
print_color "$GREEN" "πŸ”„ To resume: ./$SCRIPT_NAME $DATA_FILE -d $extensions_formatted"
print_color "$BLUE" "πŸ“„ Partial results: $LOGFILE"
fi
fi
[[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
exit $exit_code
}
# Main execution function
main() {
# Set up cleanup trap
trap cleanup EXIT INT TERM
log_verbose "Starting main execution..."
# Check dependencies and proceed
check_dependencies
parse_args "$@"
# Initialize
TEMP_DIR=$(mktemp -d)
log_verbose "Created temp directory: $TEMP_DIR"
local existing_count=$(count_existing_results)
log_verbose "Existing count: $existing_count"
local jobs_to_process
jobs_to_process=$(build_work_queue)
log_verbose "Jobs to process: $jobs_to_process"
# Check if all work is already done
if [[ $jobs_to_process -eq 0 && $existing_count -gt 0 ]]; then
log_verbose "All work already completed - showing existing results"
print_color "$YELLOW" "✨ All domain combinations already checked."
generate_table_output
exit 0
fi
# Check if there are no jobs to process
if [[ $jobs_to_process -eq 0 ]]; then
log_verbose "No jobs to process - this shouldn't happen with valid data"
error_exit "No domains to check. Verify your JSON file contains valid domain names."
fi
log_verbose "About to start processing $jobs_to_process jobs"
# Start processing
print_color "$BOLD$BLUE" "πŸš€ Starting Domain Availability Check"
print_color "$CYAN" " Input: $DATA_FILE ($TOTAL_DOMAINS domains)"
print_color "$CYAN" " Extensions: ${EXTENSIONS[*]}"
print_color "$CYAN" " Workers: $WORKERS, Retries: $RETRIES, Timeout: ${TIMEOUT}s"
print_color "$CYAN" " Jobs to process: $jobs_to_process"
echo
# Start workers
log_verbose "Starting $WORKERS worker processes..."
for ((i=1; i<=WORKERS; i++)); do
worker_process "$i" &
done
# Show progress unless quiet
if [[ "$QUIET" != "true" ]]; then
show_progress &
local progress_pid=$!
fi
# Wait for completion
log_verbose "Waiting for all workers to complete..."
wait
# Stop progress display
if [[ -n "${progress_pid:-}" ]]; then
kill "$progress_pid" 2>/dev/null || true
wait "$progress_pid" 2>/dev/null || true
fi
# Generate final report
print_color "$BOLD$GREEN" "\nπŸŽ‰ All checks completed. Generating final report..."
generate_table_output
# Cleanup progress file
[[ -f "$LOGFILE.progress" ]] && rm -f "$LOGFILE.progress"
print_color "$BOLD$GREEN" "βœ… Done!"
}
# Execute main function
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment