#!/bin/bash
# vardump - Bash library for pretty-printing a variable given by name.
# Dump given variable(s) (by name) informations and value to stdout.
#
# Author: Dave Eddy <dave@daveeddy.com>
# Date: December 18, 2024
# License: MIT
#
# Strongly edited by: Felix Hauri <bash.vardump@f-hauri.ch>
#  - Avoiding bash loops whenever possible
#  - Using associative array for colors
#  - Creating main loop to dump many variable at once
#  - Using printf instead of echo
#  - Added attributes toLower and toUpper
#  - Using array for attribute (was buggy in Dave's version)
#    - Then print all attributes verbosely without loop
# Version: 0.3.12 -- Last update: Thu Jul 15 10:46:12 CEST 2025
#
# Arguments:
#   -v                      verbose output
#   -n                      print variable names
#   -C [always|auto|never]  when to colorize output, defaults to auto
#
# shellcheck disable=SC2059   # $fStr are built from arrays to avoid bash loops

vardump() {
    local -A Color=( [rst]=$'\e[0m' [dim]=$'\e[2m' [green]=$'\e[38;5;34;1m'
		     [magenta]=$'\e[38;5;127;1m' [orange]=$'\e[38:5:208m')
    # read arguments
    local verbose=false printName=false name
    local whencolor='auto'
    local OPTIND OPTARG opt
    while getopts 'C:vn' opt; do
	case "$opt" in
	    C) whencolor=$OPTARG;;
	    v) verbose=true;;
	    n) printName=true;;
	    *) local oid=$((OPTIND-1))
	       printf >&2 \
		  'Unknown arg: "%s"!\nUsage: %s [-C auto|always|never] [-v]\n'\
		  "${!oid}" "${FUNCNAME[0]}"
	       return 1;;
	esac
    done
    shift "$((OPTIND - 1))"

    for name; do
	if [[ -z $name ]]; then
	    printf >&2 \
		  '%s: variable name(s) argument(s) missing!\n' "${FUNCNAME[0]}"
	    return 1
	fi

	# optionally load colors
	if [[ $whencolor != always ]] && [[ $whencolor != auto || ! -t 1 ]]; then
	    local -A Color=()
	fi
	Color[value]=${Color[green]}
	Color[key]=${Color[magenta]}
	Color[length]=${Color[orange]}

	# optionally print header
	$verbose && printf "%s--------------------------\nvardump: %s%s\n" \
			   "${Color[dim]}" "${Color[rst]}" "$name"

	# ensure the variable is defined
	if ! declare -p "$name" &>/dev/null; then
	    printf >&2 \
		   '%s: variable %q not defined!\n' "${FUNCNAME[0]}" "${name}"
	    return 1
	fi

	# XXX dave says - is this ideal? when the variable is a nameref (using
	# `declare -n` or `local -n`), this method will follows the nameref!!
	# Alternatively, we could parse `declare -p` output, but I think this
	# function is not intended to show reference of a nameref.

	# Take attributes separted into an array
	local attrs="${!name@a}" list _verb=false
	read -ra attrs <<< "${attrs//?/ &}"

	# optionally print the attributes
	if $verbose; then
	    printf '%sattributes:%s ' "${Color[dim]}" "${Color[rst]}"
	    if (( ${#attrs[@]} )); then
		local -A attrDscs=([a]='indexed array' [A]='associative array'
		    [r]='read-only' [i]='integer' [g]='global' [x]='exported'
		    [l]='loLower' [u]='toUpper' )
		read -ra list <<<"${attrs[@]/*/\"(&)\$\{attrDscs[&]\}\"}"
		[[ $SHELLOPTS ]] && [[ -z ${SHELLOPTS//*verbose*} ]] &&
		    _verb=true && set +v
		eval "printf -v list '%s/' ${list[*]}"
		$_verb && set -v
		printf '%s\n' "${list%/}"
	    else
		printf '%s\n' '(none)'
	    fi
	fi

	#
	# we *pray* the user doesn't use this variable name in their own code or
	# we'll hit a circular reference error (THAT WE CAN'T CATCH OURSELVES IN
	# CODE - lame)
	local -n __vardump_name="$name"

	if [[ ${attrs[*]//[!aA]} ]]; then
	    # print this as an array - indexed, sparse, associative,
	    # whatever
	    local key value
	    
	    # optionally print length
	    if $verbose; then
		printf '%slength:%s %s%s%s\nvalue: ' "${Color[dim]}"\
		       "${Color[rst]}" "${Color[length]}" ${#__vardump_name[@]}\
		       "${Color[rst]}"
	    elif $printName; then
		printf '%s%s%s=' "${Color[key]}" "$name" "${Color[rst]}"
	    fi
	    # print keys and values, without loop
	    local fStr="\t[${Color[key]}%q${Color[rst]}]="
	    fStr+="${Color[value]}%%q${Color[rst]}\n"
	    printf -v fStr "$fStr" "${!__vardump_name[@]}"
	    printf -v fStr "$fStr" "${__vardump_name[@]}"
	    printf '(\n%s\n)\n' "$fStr"
	else
	    # we are just a simple scalar value - print this as a regular,
	    # safely-quoted,  value.
	    if $verbose; then
		printf '%sstring length:%s %s%s%s\n%svalue:%s %s%q%s\n' \
		       "${Color[dim]}" "${Color[rst]}" "${Color[length]}" \
		       ${#__vardump_name} "${Color[rst]}" "${Color[dim]}" \
		       "${Color[rst]}" "${Color[value]}" "${__vardump_name}" \
		       "${Color[rst]}"
	    elif $printName; then
		printf '%s%s%s=%s%q%s\n' "${Color[key]}" "$name" \
		       "${Color[rst]}" "${Color[value]}" "${__vardump_name}" \
		       "${Color[rst]}"
	    else
		printf '%s%q%s\n' "${Color[value]}" "${__vardump_name}" \
		       "${Color[rst]}"
	    fi
	fi

	# optionally print the trailer
	$verbose && printf '%s--------------------------%s\n' \
			   "${Color[dim]}" "${Color[rst]}"
    done
    return 0
}

# Exit here if sourced
[[ $0 = "${BASH_SOURCE[0]}" ]] || { true; return 0;}

# Else, run some examples:
set -v

declare -u s
s='Some string'

declare -r READ_ONLY="This can't be changed"
declare -i int_value=5

declare -a simple_array=(foo bar baz "$(tput setaf 1)red$(tput sgr0)")
declare -ar sparse_array=([0]=hi [5]=bye [7]=ok)
declare -Ari assoc_array=([foo]=12 [bar]=34 [mPI]=3141593)

: "$s $READ_ONLY $int_value ${simple_array[*]} \
  	${sparse_array[*]} ${assoc_array[*]}"

echo 'simple vardump (colors=auto)'
vardump -C auto  s READ_ONLY int_value simple_array sparse_array assoc_array
 
echo 'vardump with names (colors=never)'
vardump -C never -n s READ_ONLY int_value simple_array sparse_array assoc_array

echo 'verbose vardump (colors=always)'
vardump -C always -v s READ_ONLY int_value simple_array sparse_array assoc_array
