-
-
Save nedzen/0fcfdf0198bb78eadb6a8abdcb7e4efe to your computer and use it in GitHub Desktop.
Domain checker
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
#!/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