#!/usr/bin/env bash # nf (netfinger): A netcat-based finger client. # Copyright 2025-2026 640kb.neocities.org # Blue Oak Model License 1.0.0 # Features: user@host and finger:// queries, HTML/Markdown render, smart wrap, # blocklists, custom shortcuts, ANSI Art viewer, classic 7-bit mode and more. # Requirements: nc, awk, less, fold (standard on most Linux systems) # Optional: pandoc, lynx (for Markdown support), GUI browser (for --mdg) # 2026-08-12: Latest update (classic mode, width variable, restructuring) # 2025-11-24: Initial release # =========================================================================== # CONFIGURATION: START # =========================================================================== # NOTE: Use lowercase true/false only. (Uppercase breaks the script.) # =========================================================================== # The wrap width (in columns) when WRAP_MODE is set to "true" or when the # -w | --wrap flags are used. Default is 80. WRAP_WIDTH=80 # When "true", enables permanent wrapping via wrap_text() function (uses "less"). # Default is 80 columns. The -w | --wrap flags achieves this on a per-request basis. WRAP_MODE=false # When set to "true", wraps output in basic HTML tags and converts plain text # URLs (http, https, gemini, finger, spartan, gopher) into clickable links. # The -r | --render flag achieves this on a per-request basis. RENDER_MODE=false # When "true", renders Markdown output using pandoc and displays in lynx (text # browser). The -m | --md flag achieves this on a per-request basis. MD_MODE=false # When "true", renders Markdown output using pandoc and opens in GUI browser # (firefox by default). The --mdg flag achieves this on a per-request basis. MDG_MODE=false # CLASSIC_MODE: Set to true for permanent 7-bit ASCII filtering (pre-parser). # Filters 8-bit bytes immediately after nc reception, before any built-in script # parser (awk, pandoc, lynx, etc) touches them. Matches the BSD finger behavior: # parsers never see 8-bit data. DEFAULT: false (process full 8-bit stream) CLASSIC_MODE=false # GUI Browsers for --mdg mode: GUI_BROWSER="firefox" # =========================================================================== # CONFIGURATION: END # =========================================================================== # =========================================================================== # SHORTCUTS: BEGIN # =========================================================================== # Some examples below. Adjust for personal use or you can delete this entire # section without affecting the script's core function. # =========================================================================== case "$1" in --ansi) "$0" ansi@happynetbox.com | head -n -2 | echo -e "$(cat)" exit 0 ;; --verse) "$0" fingerverse@happynetbox.com | less exit 0 ;; --text) "$0" textfile@typed-hole.org | less exit 0 ;; --say) "$0" say@happynetbox.com | head -n 25 exit 0 ;; # --------------------------------------------------------------------------- # happynetbox.com shortcut (--hnb) with BLOCKLIST option # Edit the "grep -v..." lines to add/remove usernames from your blocklist # --------------------------------------------------------------------------- --hnb) "$0" @happynetbox.com \ | tail -n +5 | head -n -7 \ | grep -v -i -w trollacct1 \ | grep -v -i -w spammer1 \ | grep -v -i -w trollacct2 exit 0 ;; # --------------------------------------------------------------------------- # plan.cat shortcut (--plan) with BLOCKLIST option # Edit the "grep -v..." lines to add/remove usernames from your blocklist # --------------------------------------------------------------------------- --plan) "$0" @plan.cat \ | head -n 20 \ | grep -v -i -w trollacct1 \ | grep -v -i -w spammer1 \ | grep -v -i -w trollacct2 exit 0 ;; # --------------------------------------------------------------------------- -l|--list) printf "%s\n" --ansi --verse --text --say --hnb --plan --list exit 0 ;; esac # =========================================================================== # SHORTCUTS: END # =========================================================================== if [ $# -eq 1 ] && [[ "$1" =~ ^(-h|--help|/\?|-\?)$ ]]; then echo echo " .-------------------------------------------------------------." echo " | nf (netfinger): A finger client using netcat. |" echo " |-------------------------------------------------------------|" echo " | Usage: nf user@domain.com |" echo " | nf finger://domain.com/user |" echo " |-------------------------------------------------------------|" echo " | Options: -w --wrap Smart wrap using fold & less |" echo " | -r --render HTML as
 with clickable links |"
    echo " |           -m  --md       Render Markdown with Pandoc & Lynx |"
    echo " |               --mdg      Render Markdown (GUI Browser)      |"
    echo " |           -c  --classic  Only 7-bit ASCII is parsed         |"
    echo " |                                                             |"
    echo " |           -l  --list     Displays all shortcuts (see code)  |"
    echo " |-------------------------------------------------------------|"
    echo " | Examples: nf echo@plan.cat                                  |"
    echo " |           nf @plan.cat | head -n 20                         |"
    echo " |           nf finger://happynetbox.com/spartanware           |"
    echo " |                                                             |"
    echo " |           nf -w bot@happynetbox.com                         |"
    echo " |           nf -r bot@happynetbox.com | chawan -T text/html   |"
    echo " |           nf -m markdown@happynetbox.com                    |"
    echo " |           nf -c codepage437@happynetbox.com                 |"
    echo " |                                                             |"
    echo " |           nf -l       (list shortcuts)                      |"
    echo " |           nf --ansi   (shortcut to ansi@happynetbox.com)    |"
    echo " |-------------------------------------------------------------|"
    echo " | See the configuration section at the top of the script for: |"
    echo " |                                                             |"
    echo " | Wrap width, classic mode, render, gui browser and more.     |"
    echo " '-------------------------------------------------------------'"
    echo
    exit 0
fi

FINGER_TARGET=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        -w|--wrap)
            WRAP_MODE=true
            shift
            ;;
        -r|--render)
            RENDER_MODE=true
            shift
            ;;
        -m|--md)
            MD_MODE=true
            shift
            ;;
        --mdg)
            MDG_MODE=true
            shift
            ;;
        -c|--classic)
            CLASSIC_MODE=true
            shift
            ;;
        -*)
            echo "Error: Unknown flag $1" >&2
            exit 1
            ;;
        *)
            if [ -z "$FINGER_TARGET" ]; then
                FINGER_TARGET="$1"
                shift
            else
                echo "Error: Multiple finger targets specified" >&2
                exit 1
            fi
            ;;
    esac
done

if [ -z "$FINGER_TARGET" ]; then
    echo
    echo " Error: Missing finger target" >&2
    echo " Use 'nf -h' for help" >&2
    echo
    exit 1
fi

input="$FINGER_TARGET"

if [[ "$input" == finger://* ]]; then
    path="${input#finger://}"
    
    if [[ "$path" == */* ]]; then
        host="${path%%/*}"
        user="${path#*/}"
    else
        host="$path"
        user=""
    fi
    
    if [ -n "$user" ]; then
        input="$user@$host"
    else
        input="@$host"
    fi
fi

if [[ "$input" == *://* ]]; then
    protocol="${input%%://*}"
    echo "Error: Only finger protocol is supported (got $protocol:// instead)" >&2
    exit 1
fi

wrap_text() {
    fold -s -w "${1:-$WRAP_WIDTH}" | less
}

render_html() {
    awk '{
        while(match($0, /(https?|gemini|finger|spartan|gopher):\/\/[^[:space:]<>"'"'"']*/)) {
            url = substr($0, RSTART, RLENGTH)
            after = substr($0, RSTART + RLENGTH)
            
            # ver 3.5 update: Strip trailing punctuation from all URLs EXCEPT Wikipedia.
            if (url ~ /[.,;:!?)]$/ && !(url ~ /wikipedia\.org/)) {
                after = substr(url, length(url)) after
                url = substr(url, 1, length(url) - 1)
            }
            
            printf "%s%s", substr($0, 1, RSTART - 1), url, url
            $0 = after
        }
        print
    }'
}

fetch_finger() {
    local host="$1"
    local user="$2"
    local data
    
    if [ -n "$user" ]; then
        data=$(printf '%s\r\n' "$user" | nc "$host" 79 2>/dev/null || echo "$user" | nc "$host" 79 2>/dev/null)
    else
        data=$(printf '\r\n' | nc "$host" 79 2>/dev/null || echo | nc "$host" 79 2>/dev/null)
    fi
    
    if $CLASSIC_MODE; then
        data=$(echo "$data" | tr -cd '\000-\177')
    fi
    
    printf '%s' "$data"
}

if [[ "$input" == @* ]]; then
    host="${input#@}"
    output=$(fetch_finger "$host" "")
elif [[ "$input" == *@* ]]; then
    user="${input%@*}"
    host="${input#*@}"
    output=$(fetch_finger "$host" "$user")
else
    echo "Error: Input must be in format user@host or finger://host/user" >&2
    exit 1
fi

if $MDG_MODE; then
    TMP=$(mktemp --suffix=.html) && echo "$output" | pandoc -s -f markdown -t html -o "$TMP" && $GUI_BROWSER "$TMP" && rm "$TMP"
elif $MD_MODE; then
    echo "$output" | pandoc -s -f markdown -t html | lynx -stdin

elif $RENDER_MODE; then
    echo "
"
    echo "$output" | render_html
    echo "
" elif $WRAP_MODE; then echo "$output" | wrap_text "$WRAP_WIDTH" else echo "$output" echo # blank line for improved readability of finger query fi