Created
August 27, 2025 17:32
-
-
Save supermarsx/be0fcfba150e0e374e4294ee03e0f3a1 to your computer and use it in GitHub Desktop.
Wi-Fi QR Forge, create QR codes for Wi-Fi networks fast
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 bash | |
| # Wi‑Fi QR Forge — a Wi‑Fi QR code generator with optional password creation | |
| # | |
| # Requirements: qrencode (preferred). Optional: xclip/wl-copy/pbcopy to copy payload to clipboard. | |
| # | |
| # Examples: | |
| # ./wifiqr.sh -s "MySSID" -p "s3cretpass" -t WPA -o ./mywifi.png | |
| # ./wifiqr.sh -s "Café do Bairro" -t nopass -f ansi # print QR to terminal | |
| # ./wifiqr.sh -s Net -g 16 -o wifi.svg -f svg -C # generate 16‑char password, SVG, copy payload | |
| # ./wifiqr.sh -s Net -p pass -o out.png -R # add R:1 (WPA3 transition mode disabled) | |
| # AUTO_INSTALL=1 ./wifiqr.sh -s Net -p pass -o out.png # auto-install qrencode if missing | |
| # | |
| # QR payload format used: | |
| # WIFI:S:<SSID>;T:<WEP|WPA|nopass>;P:<password>;H:<true|false>;R:1;; | |
| # - T:WPA is used for any WPA generation (WPA/WPA2/WPA3). | |
| # - R:1 disables WPA2 transition mode (WPA3 spec). Optional. | |
| # - H: is included only if specified. | |
| set -Eeuo pipefail | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Pretty UI helpers | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| if [[ -t 1 ]]; then | |
| BOLD=$(tput bold || true); DIM=$(tput dim || true); RESET=$(tput sgr0 || true) | |
| BLUE=$(tput setaf 6 || true); GREEN=$(tput setaf 2 || true); YELLOW=$(tput setaf 3 || true); RED=$(tput setaf 1 || true) | |
| else | |
| BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED="" | |
| fi | |
| die() { echo -e "${RED}${BOLD}✖${RESET} $*" >&2; exit 1; } | |
| info() { echo -e "${BLUE}${BOLD}ℹ${RESET} $*"; } | |
| ok() { echo -e "${GREEN}${BOLD}✔${RESET} $*"; } | |
| warn() { echo -e "${YELLOW}${BOLD}!${RESET} $*"; } | |
| banner() { | |
| local title="Wi‑Fi QR Forge" | |
| local line="$(printf '═%.0s' {1..66})" | |
| echo -e "${BLUE}${BOLD}╔$line╗${RESET}" | |
| printf "%s║ %-64s ║%s\n" "${BLUE}${BOLD}" "$title" "${RESET}" | |
| echo -e "${BLUE}${BOLD}╚$line╝${RESET}" | |
| } | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Dependency checks / installers | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| have() { command -v "$1" >/dev/null 2>&1; } | |
| pkg_manager="" | |
| detect_pkg_manager() { | |
| for pm in apt-get dnf yum pacman zypper apk brew port; do | |
| if have "$pm"; then pkg_manager="$pm"; return 0; fi | |
| done | |
| return 1 | |
| } | |
| install_qrencode() { | |
| detect_pkg_manager || return 1 | |
| case "$pkg_manager" in | |
| apt-get) sudo apt-get update && sudo apt-get install -y qrencode ;; | |
| dnf) sudo dnf install -y qrencode ;; | |
| yum) sudo yum install -y qrencode ;; | |
| pacman) sudo pacman -Sy --noconfirm qrencode ;; | |
| zypper) sudo zypper install -y qrencode ;; | |
| apk) sudo apk add --no-cache qrencode ;; | |
| brew) brew install qrencode ;; | |
| port) sudo port install qrencode ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| ensure_qrencode() { | |
| if have qrencode; then return 0; fi | |
| warn "qrencode not found. It's required to render the QR." | |
| if [[ "${AUTO_INSTALL:-0}" == "1" || "${AUTO_INSTALL:-false}" == "true" || "${DO_INSTALL:-0}" == "1" ]]; then | |
| info "Attempting to install qrencode (package manager: ${pkg_manager:-auto-detect})…" | |
| install_qrencode || die "Unable to install qrencode automatically. Install it manually and retry." | |
| ok "qrencode installed." | |
| else | |
| info "Install suggestions:" | |
| echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install qrencode" | |
| echo " Fedora/RHEL: sudo dnf install qrencode" | |
| echo " Arch: sudo pacman -S qrencode" | |
| echo " Alpine: sudo apk add qrencode" | |
| echo " macOS: brew install qrencode" | |
| die "Rerun after installing qrencode (or set AUTO_INSTALL=1)." | |
| fi | |
| } | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Escaping per ZXing Wi‑Fi QR standard (escape \\ ; , : ") | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| esc() { | |
| local s=$1 | |
| s=${s//\\/\\\\} | |
| s=${s//;/\\;} | |
| s=${s//,/\\,} | |
| s=${s//:/\\:} | |
| s=${s//\"/\\\"} | |
| printf '%s' "$s" | |
| } | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Password generator (safe ASCII, excludes spaces and problematic shell chars) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| gen_password() { | |
| local len=${1:-16} | |
| local charset='A-Za-z0-9!@#$%&*+=?^_~.-' | |
| tr -dc "$charset" </dev/urandom | head -c "$len" | |
| } | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Usage | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| usage() { | |
| cat <<'USAGE' | |
| Usage: wifiqr.sh -s <SSID> [-p <password>] [-t <WPA|WEP|nopass>] \ | |
| [-H true|false] [-R] [-o <file>] [-f png|svg|ansi] \ | |
| [-g <len>] [-m <margin>] [-S <scale>] [-C] [--install] | |
| Options: | |
| -s SSID Network name (SSID) | |
| -p PASSWORD Wi‑Fi password (ignored for -t nopass). If omitted and -g is set, a password is generated. | |
| -t TYPE Security type (default: WPA). Use 'nopass' for open networks. WPA covers WPA/WPA2/WPA3. | |
| -H BOOL Hidden SSID (true/false). Omit for visible. | |
| -R Add R:1 tag (WPA3 transition mode disabled). Optional. | |
| -o FILE Output file (default: wifi-<ssid>-<timestamp>.png unless -f ansi). | |
| -f FORMAT Output format: png (default), svg, or ansi (prints to terminal). | |
| -g N Generate a random password of length N. | |
| -m MARGIN Quiet zone (default: 4 modules). | |
| -S SCALE Pixel scale per module (default: 8 for png/svg, 1 for ansi). | |
| -C Copy QR payload string to clipboard (pbcopy/xclip/wl-copy if available). | |
| --install Attempt to auto-install qrencode if missing (or set AUTO_INSTALL=1). | |
| -h, --help Show this help. | |
| Examples: | |
| wifiqr.sh -s "MyHome" -p "CorrectHorseBatteryStaple" -t WPA -o mywifi.png | |
| wifiqr.sh -s "PublicPlaza" -t nopass -f ansi | |
| wifiqr.sh -s OfficeNet -g 20 -o office.svg -f svg -C | |
| wifiqr.sh -s WPA3Net -p supersecure -t WPA -R -o wpa3.png | |
| USAGE | |
| } | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Parse args | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| ssid=""; password=""; type="WPA"; hidden=""; REQ_WPA3=0; outfile=""; format="png"; genlen=""; margin=4; scale="" | |
| DO_INSTALL=0 | |
| LONGOPTS=("help" "install") | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --help) usage; exit 0 ;; | |
| --install) DO_INSTALL=1 ;; | |
| esac | |
| done | |
| while getopts ":s:p:t:H:R:o:f:g:m:S:C h" opt; do | |
| case "$opt" in | |
| s) ssid=$OPTARG ;; | |
| p) password=$OPTARG ;; | |
| t) type=$OPTARG ;; | |
| H) hidden=$OPTARG ;; | |
| R) REQ_WPA3=1 ;; | |
| o) outfile=$OPTARG ;; | |
| f) format=$OPTARG ;; | |
| g) genlen=$OPTARG ;; | |
| m) margin=$OPTARG ;; | |
| S) scale=$OPTARG ;; | |
| C) COPY=1 ;; | |
| h) usage; exit 0 ;; | |
| :) die "Option -$OPTARG requires an argument. See -h." ;; | |
| \?) die "Unknown option: -$OPTARG. See -h." ;; | |
| esac | |
| done | |
| banner | |
| [[ -z "$ssid" ]] && { usage; die "SSID (-s) is required."; } | |
| # Normalize/validate | |
| shopt -s nocasematch | |
| case "$type" in | |
| wpa|wpa2|wpa3|WPA|WPA2|WPA3) type=WPA ;; | |
| wep|WEP) type=WEP ;; | |
| nopass|NOPASS) type=nopass ;; | |
| *) die "Unsupported type: $type (use WPA/WEP/nopass)" ;; | |
| esac | |
| shopt -u nocasematch | |
| if [[ -n "$genlen" && -z "$password" && "$type" != "nopass" ]]; then | |
| password=$(gen_password "$genlen") | |
| ok "Generated password (${#password} chars)." | |
| fi | |
| if [[ "$type" != "nopass" && -z "$password" ]]; then | |
| warn "No password provided. For open networks use -t nopass; otherwise add -p or -g." | |
| fi | |
| if [[ "$type" == "WPA" ]]; then | |
| if [[ -n "$password" && ${#password} -lt 8 ]]; then | |
| warn "WPA passwords should be at least 8 chars." | |
| fi | |
| fi | |
| # Build payload with escaping | |
| SSID_ESC=$(esc "$ssid") | |
| PAYLOAD="WIFI:S:${SSID_ESC};T:${type};" | |
| if [[ "$type" != "nopass" && -n "$password" ]]; then | |
| PASS_ESC=$(esc "$password") | |
| PAYLOAD+="P:${PASS_ESC};" | |
| fi | |
| if [[ -n "$hidden" ]]; then | |
| case "$hidden" in | |
| true|false|TRUE|FALSE) PAYLOAD+="H:${hidden,,};" ;; | |
| *) warn "-H should be true or false; ignoring." ;; | |
| esac | |
| fi | |
| if [[ "$REQ_WPA3" == 1 ]]; then | |
| PAYLOAD+="R:1;" | |
| fi | |
| PAYLOAD+=";" | |
| # Copy to clipboard if requested | |
| copy_payload() { | |
| if have pbcopy; then printf '%s' "$PAYLOAD" | pbcopy && ok "Payload copied to clipboard (macOS)." && return | |
| fi | |
| if have wl-copy; then printf '%s' "$PAYLOAD" | wl-copy && ok "Payload copied to clipboard (Wayland)." && return | |
| fi | |
| if have xclip; then printf '%s' "$PAYLOAD" | xclip -selection clipboard && ok "Payload copied to clipboard (X11)." && return | |
| fi | |
| warn "Clipboard tool not found (pbcopy/wl-copy/xclip). Skipping copy." | |
| } | |
| [[ "${COPY:-0}" == 1 ]] && copy_payload || true | |
| # Determine defaults for output | |
| if [[ -z "$scale" ]]; then | |
| if [[ "$format" == "ansi" ]]; then scale=1; else scale=8; fi | |
| fi | |
| if [[ -z "$outfile" && "$format" != "ansi" ]]; then | |
| ts=$(date +%Y%m%d-%H%M%S) | |
| safe_ssid=${ssid//[^A-Za-z0-9._-]/_} | |
| ext=$([[ "$format" == "svg" ]] && echo svg || echo png) | |
| outfile="wifi-${safe_ssid}-${ts}.${ext}" | |
| fi | |
| # Render | |
| case "$format" in | |
| ansi) | |
| ensure_qrencode | |
| info "Rendering QR to terminal (ANSI)…" | |
| echo | |
| qrencode -t ANSIUTF8 -m "$margin" -s "$scale" "$PAYLOAD" | |
| echo | |
| ;; | |
| png) | |
| ensure_qrencode | |
| info "Rendering PNG → ${outfile}" | |
| qrencode -t PNG -m "$margin" -s "$scale" -o "$outfile" "$PAYLOAD" | |
| ok "Saved ${outfile}" | |
| ;; | |
| svg) | |
| ensure_qrencode | |
| info "Rendering SVG → ${outfile}" | |
| qrencode -t SVG -m "$margin" -s "$scale" -o "$outfile" "$PAYLOAD" | |
| ok "Saved ${outfile}" | |
| ;; | |
| *) die "Unknown format: $format (use png|svg|ansi)" ;; | |
| esac | |
| # Summary box | |
| summary_line() { printf "%-14s %s\n" "$1" "$2"; } | |
| printf "${DIM}──────────────────────────────────────────────────────────────────${RESET}\n" | |
| summary_line "SSID" "$ssid" | |
| summary_line "Type" "$type" | |
| [[ "$type" != "nopass" && -n "$password" ]] && summary_line "Password" "$password" | |
| [[ -n "$hidden" ]] && summary_line "Hidden" "${hidden,,}" | |
| [[ "$REQ_WPA3" == 1 ]] && summary_line "R" "1" | |
| summary_line "Payload" "$PAYLOAD" | |
| if [[ "$format" != "ansi" ]]; then summary_line "File" "$outfile"; fi | |
| printf "${DIM}──────────────────────────────────────────────────────────────────${RESET}\n" | |
| ok "Done. Point your camera at the QR to join the network." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment