From a935e484d906b62d9717188ed69af6d363fcbf6cef82c0628669b6e8c9548b41 Mon Sep 17 00:00:00 2001 From: Heath Petersen Date: Fri, 7 Aug 2026 16:59:32 -0500 Subject: [PATCH] create jsonvar.bash --- scraps/jsonvar.bash | 257 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 scraps/jsonvar.bash diff --git a/scraps/jsonvar.bash b/scraps/jsonvar.bash new file mode 100644 index 0000000..2b9a2f1 --- /dev/null +++ b/scraps/jsonvar.bash @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# +# Serialize bash variables to JSON output +# +# # Created +# Author: Dave Eddy +# Date: July 09, 2026 +# License: MIT +# +# # Contributors +# - Dave Eddy + +_jv-usage() { + local usage + read -r -d '' usage <<-EOF + Usage: jsonvar [-aev] [[name], ...] + + Serialize bash variables to JSON output + + Options + -a show all variables + -e show only exported variables + -v show only the values of the variables + -h show this message and exit + EOF + echo "$usage" +} + +_jv-json-encode-string() { + local s=$1 + + local LC_ALL=C + local -A table=() + + # we can start at 1 because bash variables can't have nul bytes in them + local hex byte esc i + for ((i = 1; i < 0x20; i++)); do + printf -v hex '%02x' "$i" + + printf -v byte '%b' "\\x$hex" + printf -v esc '\\u%04x' "$i" + table[$byte]=$esc + done + + table[$'\b']='\b' + table[$'\t']='\t' + table[$'\n']='\n' + table[$'\f']='\f' + table[$'\r']='\r' + + table['\']='\\' + table['"']='\"' + + # serialize the string + local out='' + local len=${#s} + local c + for ((i = 0; i < len; i++)); do + c=${s:i:1} + esc=${table[$c]} + + if [[ -n $esc ]]; then + # lookup table matched for this byte + out+=$esc + else + # no lookup table match, byte falls through + out+=$c + fi + done + + + printf '"%s"' "$out" +} + +_jv-encode-variable() { + local _jv_name=$1 + local -n _jv_ref=$_jv_name + local _jv_attrs=${_jv_ref@a} + + case "$_jv_attrs" in + *a*) # process indexed array + echo -n '[' + local _jv_value _jv_i=0 + for _jv_value in "${_jv_ref[@]}"; do + ((_jv_i++)) + + # check member type + if [[ $_jv_attrs == *i* ]]; then + printf '%d' "$_jv_value" + else + _jv-json-encode-string "$_jv_value" + fi + + if ((_jv_i < ${#_jv_ref[@]})); then + echo -n ', ' + fi + done + echo -n ']' + ;; + *A*) # process associative array + echo -n '{' + local _jv_key _jv_value _jv_i=0 + for _jv_key in "${!_jv_ref[@]}"; do + ((_jv_i++)) + + _jv_value=${_jv_ref[$_jv_key]} + + _jv-json-encode-string "$_jv_key" + echo -n ': ' + + if [[ $_jv_attrs == *i* ]]; then + printf '%d' "$_jv_value" + else + _jv-json-encode-string "$_jv_value" + fi + + if ((_jv_i < ${#_jv_ref[@]})); then + echo -n ', ' + fi + done + echo -n '}' + ;; + *i*) # process integer + echo -n "$_jv_ref" + ;; + *) # anything else, it's probably a string lol + _jv-json-encode-string "$_jv_ref" + ;; + esac + +} + +jsonvar() { + local _jv_all='false' + local _jv_exported='false' + local _jv_value='false' + + # get arguments from user + local OPTIND OPTARG _jv_opt + while getopts 'aevh' _jv_opt; do + case "$_jv_opt" in + a) _jv_all='true';; + e) _jv_exported='true';; + v) _jv_value='true';; + h) _jv-usage; return 0;; + *) _jv-usage >&2; return 2;; + esac + done + shift "$((OPTIND - 1))" + + local _jv_key + + # figure out what variables to look at + local -a _jv_variables + if $_jv_all; then + readarray -t _jv_variables < <(compgen -v) + elif $_jv_exported; then + readarray -t _jv_variables < <(compgen -e) + else + _jv_variables=("$@") + + # ensure the user gave us *something* + if (( ${#_jv_variables[@]} == 0 )); then + echo 'variable name or flag required' >&2 + _jv-usage >&2 + return 2 + fi + + # check variables given + local _jv_error='false' + for _jv_key in "${_jv_variables[@]}"; do + # warn the user if they gave us an internal name + if [[ $_jv_key == _jv_* ]]; then + echo "[error] invalid internal variable '$_jv_key'" >&2 + _jv_error='true' + fi + + # check to make sure the variable is defined + if ! declare -p "$_jv_key" &>/dev/null; then + echo "[error] variable '$_jv_key' not defined" >&2 + _jv_error='true' + fi + done + + if $_jv_error; then + return 1 + fi + fi + + # loop the variables first to filter out hidden / internal var names + local _jv_i + local _jv_len=${#_jv_variables[@]} + for ((_jv_i = 0; _jv_i < _jv_len; _jv_i++)); do + _jv_key=${_jv_variables[_jv_i]} + + # filter out internal variables by name + if [[ $_jv_key == _jv_* ]]; then + unset '_jv_variables[_jv_i]' + continue + fi + + # variable name was good, do nothing + done + + # loop the remaining variables and format them + $_jv_value || echo '{' + _jv_i=0 + for _jv_key in "${_jv_variables[@]}"; do + ((_jv_i++)) + + if ! $_jv_value; then + # indent + echo -n ' ' + + # print the key + _jv-json-encode-string "$_jv_key" + echo -n ': ' + fi + + # print the value + _jv-encode-variable "$_jv_key" + + # optionally print the comma + if ! $_jv_value && ((_jv_i < ${#_jv_variables[@]})); then + echo -n ',' + fi + echo + done + $_jv_value || echo '}' +} + +_jv-complete() { + COMPREPLY=( + # add all variables + $(compgen -v -- "${COMP_WORDS[COMP_CWORD]}") + + # add the individual flags + $(compgen -W '-a -e -v -h' -- "${COMP_WORDS[COMP_CWORD]}") + ) +} + +if ( return 0 &>/dev/null ); then + # we are being sourced + complete -F _jv-complete jsonvar +else + # we are being executed directly + declare -a test_indexed=(a b c) + declare -a test_sparse=(a b c [67]=d) + declare -A test_assoc=([a]=1 [b]=2 [c]=3) + declare -i test_int=67 + declare -- test_string='hello world' + + declare -ai test_indexed_ints=(0 1 2 0xff foo bar baz) + declare -Ai test_assoc_ints=([foo]=0 [bar]=1 [baz]=0xff [bat]=foo) + + jsonvar "$@" +fi