#!/usr/bin/env bash # ugi — encode, decode, and convert UGI strings set -euo pipefail VERSION="0.1.0" # ── Registry path ──────────────────────────────────────────────────────────── REGISTRY="${UGI_REGISTRY:-}" if [[ -z "$REGISTRY" ]]; then # Try common locations relative to script SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for candidate in \ "$SCRIPT_DIR/ugi_registry_v0.json" \ "$SCRIPT_DIR/../ugi-spec/ugi_registry_v0.json" \ "$SCRIPT_DIR/ugi-spec/ugi_registry_v0.json"; do if [[ -f "$candidate" ]]; then REGISTRY="$candidate" break fi done fi require_registry() { if [[ -z "$REGISTRY" || ! -f "$REGISTRY" ]]; then echo "error: registry not found. set UGI_REGISTRY or place ugi_registry_v0.json nearby." >&2 exit 1 fi } # ── Helpers ────────────────────────────────────────────────────────────────── jq_check() { if ! command -v jq &>/dev/null; then echo "error: jq is required. install it: https://jqlang.github.io/jq/" >&2 exit 1 fi } # ── Decode ─────────────────────────────────────────────────────────────────── decode_uri() { local uri="$1" require_registry jq_check # Strip ugi: prefix, split header and fields local rest="${uri#ugi:}" local version="${rest%%@*}" rest="${rest#*@}" local handle="${rest%%:*}" local fields_str="${rest#*:}" printf "Version: %s\nHandle: @%s\n\n" "$version" "$handle" printf "%-25s %-8s %s\n" "Field" "Code" "Decoded" printf "%s\n" "----------------------------------------------------------------------" IFS=',' read -ra field_parts <<< "$fields_str" for part in "${field_parts[@]}"; do [[ -z "$part" ]] && continue local code="${part:0:1}" local rest_field="${part:1}" decode_single_field "$code" "$rest_field" "$part" done } decode_single_field() { local code="$1" rest="$2" raw="$3" local name type name=$(jq -r --arg c "$code" '.fields[$c].name // "Unknown"' "$REGISTRY") type=$(jq -r --arg c "$code" '.fields[$c].type // "unknown"' "$REGISTRY") case "$type" in direct) decode_direct "$code" "$rest" "$name" ;; special) decode_special "$code" "$rest" "$name" ;; multi) decode_multi "$code" "$rest" "$name" ;; single) decode_multi "$code" "$rest" "$name" ;; *) printf "%-25s %-8s %s\n" "$name" "$code" "$rest" ;; esac } decode_direct() { local code="$1" rest="$2" name="$3" [[ -z "$rest" ]] && return local digit="${rest:0:1}" local mods="${rest:1}" local label # Try custom values first, then scale_labels, then global scale label=$(jq -r --arg c "$code" --arg d "$digit" ' .fields[$c].values[$d].label // .fields[$c].scale_labels[$d] // empty ' "$REGISTRY" 2>/dev/null || true) if [[ -z "$label" ]]; then local scale_type scale_type=$(jq -r --arg c "$code" '.fields[$c].scale_type // ""' "$REGISTRY") if [[ -n "$scale_type" && "$scale_type" != "custom" ]]; then label=$(jq -r --arg d "$digit" --arg st "$scale_type" ' .scale.values[] | select(.value == ($d | tonumber)) | .[$st] // empty ' "$REGISTRY" 2>/dev/null || true) fi fi [[ -z "$label" ]] && label="$digit" # Parse modifiers local mod_str="" if [[ "$mods" == *"/"* ]]; then local alt_digit="${mods#*/}" alt_digit="${alt_digit:0:1}" local alt_label alt_label=$(jq -r --arg c "$code" --arg d "$alt_digit" ' .fields[$c].values[$d].label // $d ' "$REGISTRY" 2>/dev/null || true) label="$label / $alt_label" mods="${mods//\/$alt_digit/}" fi [[ "$mods" == *'$'* ]] && mod_str+=" [\$]" if [[ "$mods" == *"+"* ]]; then local asp="${mods#*+}" asp="${asp:0:1}" mod_str+=" [+$asp]" fi printf "%-25s %-8s %s%s\n" "$name" "${code}${digit}" "$label" "$mod_str" } decode_special() { local code="$1" rest="$2" name="$3" case "$code" in g) # Geek type: domain/domain/$ local decoded="" IFS='/' read -ra domains <<< "$rest" for d in "${domains[@]}"; do local paid="" local dname="$d" [[ "$d" == *'$' ]] && { paid=" (\$)"; dname="${d%\$}"; } local resolved if [[ "$dname" == "~"* ]]; then resolved="$dname" else resolved=$(jq -r --arg sid "$dname" ' [.fields.g.sub_id_groups[].sub_ids[$sid] // empty] | first // $sid ' "$REGISTRY" 2>/dev/null || echo "$dname") # Handle string vs object if [[ "$resolved" == "{"* ]]; then resolved=$(echo "$resolved" | jq -r '.name // empty' 2>/dev/null || echo "$dname") fi fi [[ -n "$decoded" ]] && decoded+=", " decoded+="${resolved}${paid}" done printf "%-25s %-8s %s\n" "$name" "$code" "$decoded" ;; b|v) IFS='/' read -ra dims <<< "$rest" local dim_names dim_labels="" dim_names=$(jq -r --arg c "$code" '.fields[$c].dimensions | keys_unsorted[]' "$REGISTRY") local i=0 while IFS= read -r dim_name; do local val="${dims[$i]:-}" if [[ -n "$val" ]]; then local dim_label dim_label=$(jq -r --arg c "$code" --arg dn "$dim_name" --arg v "$val" ' .fields[$c].dimensions[$dn].values[$v] // $v ' "$REGISTRY") [[ -n "$dim_labels" ]] && dim_labels+="; " dim_labels+="${dim_name}: ${dim_label}" fi i=$((i + 1)) || true done <<< "$dim_names" printf "%-25s %-8s %s\n" "$name" "${code}${rest}" "$dim_labels" ;; l) # Languages: 2-char code + digit + mods, repeating decode_multi_inner "$code" "$rest" "$name" 2 ;; esac } decode_multi() { local code="$1" rest="$2" name="$3" decode_multi_inner "$code" "$rest" "$name" 0 } decode_multi_inner() { local code="$1" rest="$2" name="$3" fixed_len="$4" local entries="" local i=0 len=${#rest} while (( i < len )); do local ch="${rest:$i:1}" local sub_id="" if [[ "$ch" == "~" ]]; then # Custom sub-ID local j=$((i + 1)) while (( j < len )) && [[ "${rest:$j:1}" =~ [a-zA-Z] ]]; do j=$((j + 1)) done sub_id="${rest:$i:$((j - i))}" i=$j elif [[ "$ch" =~ [a-zA-Z] ]]; then if (( fixed_len > 0 )); then sub_id="${rest:$i:$fixed_len}" i=$((i + fixed_len)) else local j=$((i)) while (( j < len )) && [[ "${rest:$j:1}" =~ [a-zA-Z] ]]; do j=$((j + 1)) done sub_id="${rest:$i:$((j - i))}" i=$j fi else i=$((i + 1)) continue fi # Resolve name local resolved if [[ "$sub_id" == "~"* ]]; then resolved="$sub_id" else resolved=$(jq -r --arg c "$code" --arg sid "$sub_id" ' ((.fields[$c].sub_ids[$sid] // null) as $flat | if $flat then if ($flat | type) == "object" then $flat.name else $flat end else null end) // (if .fields[$c].sub_id_groups then [.fields[$c].sub_id_groups[].sub_ids[$sid] // empty] | first | if type == "object" then .name else . end else null end) // (.fields[$c].common_codes[$sid] // null) // $sid ' "$REGISTRY" 2>/dev/null || echo "$sub_id") fi # Read digit local digit="" if (( i < len )) && [[ "${rest:$i:1}" =~ [0-7] ]]; then digit="${rest:$i:1}" i=$((i + 1)) fi # Read modifiers local mod_str="" if (( i < len )) && [[ "${rest:$i:1}" == '$' ]]; then mod_str+=" [\$]" i=$((i + 1)) fi if (( i < len )) && [[ "${rest:$i:1}" == "+" ]]; then i=$((i + 1)) if (( i < len )) && [[ "${rest:$i:1}" =~ [0-7] ]]; then mod_str+=" [+${rest:$i:1}]" i=$((i + 1)) fi fi # Get label for digit local label="" if [[ -n "$digit" ]]; then label=$(jq -r --arg c "$code" --arg d "$digit" ' .fields[$c].values[$d].label // .fields[$c].scale_labels[$d] // empty ' "$REGISTRY" 2>/dev/null || true) if [[ -z "$label" ]]; then local st st=$(jq -r --arg c "$code" '.fields[$c].scale_type // ""' "$REGISTRY") if [[ -n "$st" && "$st" != "custom" ]]; then label=$(jq -r --arg d "$digit" --arg st "$st" ' .scale.values[] | select(.value == ($d | tonumber)) | .[$st] // empty ' "$REGISTRY" 2>/dev/null || true) fi fi [[ -z "$label" ]] && label="$digit" fi [[ -n "$entries" ]] && entries+=", " if [[ -n "$digit" ]]; then entries+="${resolved}=${label}${mod_str}" else entries+="${resolved}${mod_str}" fi done printf "%-25s %-8s %s\n" "$name" "$code" "$entries" } # ── Convert ────────────────────────────────────────────────────────────────── convert_uri_to_block() { local uri="$1" require_registry jq_check local rest="${uri#ugi:}" local version="${rest%%@*}" rest="${rest#*@}" local handle="${rest%%:*}" local fields_str="${rest#*:}" echo "------- BEGIN UGI BLOCK -------" local first_line="v:${version} @${handle}" local -a remaining=() IFS=',' read -ra parts <<< "$fields_str" for part in "${parts[@]}"; do [[ -z "$part" ]] && continue local code="${part:0:1}" if [[ "$code" == "g" ]]; then first_line+=" G${part:1}" else remaining+=("$part") fi done echo "$first_line" # Group by category local -A cat_fields local -a cat_order=(identity appearance tech stance entertainment lifestyle) for cat in "${cat_order[@]}"; do cat_fields[$cat]="" done for part in "${remaining[@]}"; do local code="${part:0:1}" local cat cat=$(jq -r --arg c "$code" '.fields[$c].category // "tech"' "$REGISTRY") local ftype ftype=$(jq -r --arg c "$code" '.fields[$c].type // "direct"' "$REGISTRY") local uc="${code^^}" if [[ "$ftype" == "multi" || "$ftype" == "single" ]]; then # Expand merged entries local expanded expanded=$(expand_multi_block "$uc" "${part:1}" "$code") cat_fields[$cat]+="${cat_fields[$cat]:+ }${expanded}" else cat_fields[$cat]+="${cat_fields[$cat]:+ }${uc}${part:1}" fi done for cat in "${cat_order[@]}"; do [[ -n "${cat_fields[$cat]:-}" ]] && echo "${cat_fields[$cat]}" done echo "-------- END UGI BLOCK --------" } expand_multi_block() { local uc="$1" rest="$2" code="$3" local result="" i=0 len=${#rest} local fixed_len=0 [[ "$code" == "l" ]] && fixed_len=2 while (( i < len )); do local ch="${rest:$i:1}" local sub_id="" tail="" if [[ "$ch" == "~" ]]; then local j=$((i + 1)) while (( j < len )) && [[ "${rest:$j:1}" =~ [a-zA-Z] ]]; do j=$((j + 1)); done sub_id="${rest:$i:$((j - i))}" i=$j elif [[ "$ch" =~ [a-zA-Z] ]]; then if (( fixed_len > 0 )); then sub_id="${rest:$i:$fixed_len}" i=$((i + fixed_len)) else local j=$i while (( j < len )) && [[ "${rest:$j:1}" =~ [a-zA-Z] ]]; do j=$((j + 1)); done sub_id="${rest:$i:$((j - i))}" i=$j fi else i=$((i + 1)); continue fi # digit + modifiers while (( i < len )) && [[ "${rest:$i:1}" =~ [0-9\$+] ]]; do tail+="${rest:$i:1}" i=$((i + 1)) done result+="${result:+ }${uc}${sub_id}${tail}" done echo "$result" } convert_block_to_uri() { local block="$1" require_registry jq_check local version="" handle="" local -a field_parts=() while IFS= read -r line; do line="${line#"${line%%[![:space:]]*}"}" # trim leading whitespace [[ "$line" == "---"*"BEGIN"* ]] && continue [[ "$line" == "---"*"END"* ]] && continue [[ -z "$line" ]] && continue for token in $line; do if [[ "$token" == "v:"* ]]; then version="${token#v:}" elif [[ "$token" == "@"* ]]; then handle="${token#@}" elif [[ "${token:0:1}" =~ [A-Za-z] ]]; then field_parts+=("$token") fi done done <<< "$block" # Merge repeated field codes local -A merged local -a order=() for part in "${field_parts[@]}"; do local code="${part:0:1}" code="${code,,}" # lowercase if [[ -z "${merged[$code]+x}" ]]; then merged[$code]="${part:1}" order+=("$code") else merged[$code]+="${part:1}" fi done local fields_str="" for code in "${order[@]}"; do [[ -n "$fields_str" ]] && fields_str+="," fields_str+="${code}${merged[$code]}" done echo "ugi:${version}@${handle}:${fields_str}" } # ── Fields ─────────────────────────────────────────────────────────────────── list_fields() { require_registry jq_check printf "%-6s %-25s %-15s %s\n" "Code" "Field" "Type" "Category" printf "%s\n" "------------------------------------------------------------" jq -r ' .fields | to_entries | sort_by(.key)[] | "\(.key)\t\(.value.name)\t\(.value.type)\t\(.value.category)" ' "$REGISTRY" | while IFS=$'\t' read -r code fname ftype fcat; do printf "%-6s %-25s %-15s %s\n" "$code" "$fname" "$ftype" "$fcat" done # Reserved codes jq -r '.format.reserved_codes[]' "$REGISTRY" 2>/dev/null | sort | while read -r code; do printf "%-6s %-25s %-15s %s\n" "$code" "(reserved)" "—" "—" done } # ── Encode (interactive) ──────────────────────────────────────────────────── encode_interactive() { require_registry jq_check echo "=== UGI Encoder ===" echo # Handle read -rp "Handle (username): " handle [[ -z "$handle" ]] && { echo "Handle is required." >&2; exit 1; } local -a parts=() # Geek type (mandatory) echo echo "--- Geek Specialization (g) --- [mandatory]" echo "Enter domain codes separated by /. Use ~ for custom, \$ after domain for paid." echo "Example: cs\$/ai/wr~fermenting" echo echo "Available domains:" jq -r ' .fields.g.sub_id_groups | to_entries[] | " \(.value.label): " + ([.value.sub_ids | to_entries[:6][] | "\(.key)=\(if (.value|type) == "object" then .value.name else .value end)"] | join(", ")) + "..." ' "$REGISTRY" echo read -rp "Geek domains: " geek [[ -z "$geek" ]] && { echo "Geek type is required." >&2; exit 1; } parts+=("g${geek}") # Other fields local field_codes field_codes=$(jq -r '.fields | keys[] | select(. != "g")' "$REGISTRY" | sort) for code in $field_codes; do local name type name=$(jq -r --arg c "$code" '.fields[$c].name' "$REGISTRY") type=$(jq -r --arg c "$code" '.fields[$c].type' "$REGISTRY") echo echo "--- ${name} (${code}) --- [s=skip]" case "$type" in direct) # Show scale jq -r --arg c "$code" ' .fields[$c].values // {} | to_entries | sort_by(.key)[] | " \(.key): \(.value.label // .value)" ' "$REGISTRY" 2>/dev/null || true read -rp "Value (0-7, s=skip): " val [[ "$val" == "s" || -z "$val" ]] && continue [[ "$val" =~ ^[0-7]$ ]] || { echo "Invalid, skipping."; continue; } parts+=("${code}${val}") ;; special) case "$code" in b) echo " Height and width, each 0-7" read -rp " Height (0-7, s=skip): " h [[ "$h" == "s" || -z "$h" ]] && continue read -rp " Width (0-7): " w [[ -z "$w" ]] && continue parts+=("b${h}/${w}") ;; v) echo " Social: 0=far-right auth → 7=far-left lib" echo " Economic: 0=unreg capitalism → 7=full socialism" read -rp " Social (0-7, s=skip): " s [[ "$s" == "s" || -z "$s" ]] && continue read -rp " Economic (0-7): " e [[ -z "$e" ]] && continue parts+=("v${s}/${e}") ;; l) echo " Enter ISO 639-1 codes with ratings, no spaces." echo " Example: en7ar6de3" read -rp " Languages (s=skip): " langs [[ "$langs" == "s" || -z "$langs" ]] && continue parts+=("l${langs}") ;; esac ;; multi|single) jq -r --arg c "$code" ' ((.fields[$c].sub_ids // {}) | to_entries[:8][] | "\(.key)=\(if (.value|type) == "object" then .value.name else .value end)"), ((.fields[$c].sub_id_groups // {}) | to_entries[] | .value.sub_ids | to_entries[:4][] | "\(.key)=\(if (.value|type) == "object" then .value.name else .value end)") ' "$REGISTRY" 2>/dev/null | head -12 | sed 's/^/ /' echo " ..." if [[ "$type" == "single" ]]; then echo " Enter one sub-ID + rating. Example: is6" else echo " Enter sub-ID+rating pairs. Example: py6rs5js4" fi read -rp " ${name} (s=skip): " val [[ "$val" == "s" || -z "$val" ]] && continue parts+=("${code}${val}") ;; esac done local uri uri="ugi:0@${handle}:$(IFS=,; echo "${parts[*]}")" echo echo "=== Result ===" echo echo "$uri" echo convert_uri_to_block "$uri" } # ── Main ───────────────────────────────────────────────────────────────────── usage() { cat <<'EOF' ugi — UGI (Unified Geek Identifier) CLI tool Usage: ugi decode Decode a UGI string (URI or block) ugi convert Convert between URI and block format ugi encode Interactive encoder ugi fields List all field codes Aliases: d=decode, c=convert, e=encode, f=fields Environment: UGI_REGISTRY Path to ugi_registry_v0.json Options: -h, --help Show this help -v, --version Show version Reads from stdin if no string argument given (decode/convert). EOF } main() { [[ $# -eq 0 ]] && { usage; exit 0; } case "$1" in -h|--help) usage; exit 0 ;; -v|--version) echo "ugi $VERSION"; exit 0 ;; decode|d) shift local input="${1:-}" [[ -z "$input" ]] && input="$(cat)" if [[ "${input,,}" == "ugi:"* ]]; then decode_uri "$input" elif [[ "$input" == *"BEGIN UGI"* || "$input" == *"v:"* ]]; then # Parse block to URI first, then decode local uri uri=$(convert_block_to_uri "$input") decode_uri "$uri" else echo "error: unrecognized format" >&2; exit 1 fi ;; convert|c) shift local input="${1:-}" [[ -z "$input" ]] && input="$(cat)" if [[ "${input,,}" == "ugi:"* ]]; then convert_uri_to_block "$input" elif [[ "$input" == *"BEGIN UGI"* || "$input" == *"v:"* ]]; then convert_block_to_uri "$input" else echo "error: unrecognized format" >&2; exit 1 fi ;; encode|e) encode_interactive ;; fields|f) list_fields ;; *) echo "error: unknown command '$1'" >&2; usage; exit 1 ;; esac } main "$@"