diff --git a/lib/dns.sh b/lib/dns.sh deleted file mode 100644 index dded5b0..0000000 --- a/lib/dns.sh +++ /dev/null @@ -1,658 +0,0 @@ -#!/usr/bin/env bash - -#################### -# config variables # -#################### - -[ -z "${!DNS_KEYS[*]}" ] && declare -A DNS_KEYS=() -[ -z "${!BASE_CONFIG[*]}" ] && declare -A BASE_CONFIG=() -[ -z "${!ZONE_TEMPLATES[*]}" ] && declare -A ZONE_TEMPLATES=() - -DNS_IP=${DNS_IP:-127.0.0.1} - -DIG=${DIG:-$(which dig)} || exit 1 -IDN2=${IDN2:-$(which idn2)} || exit 1 -JQ=${JQ:-$(which jq)} || exit 1 -NAMED_CHECKCONF=${NAMED_CHECKCONF:-$(which named-checkconf)} || exit 1 -NSUPDATE=${NSUPDATE:-$(which nsupdate)} || exit 1 -RNDC=${RNDC:-$(which rndc)} || exit 1 - -TERMINAL_WITH=${MAX_TERMINAL_WITH:-$($(which stty) size | cut -d " " -f 2)} || exit 1 - -#################### -# global variables # -#################### - -# List of managable DNS record types -declare -a DNS_RECORD_TYPES=("A" "AAAA" "CAA" "CDS" "CNAME" "DNAME" "DS" "MX" "NS" "PTR" "SRV" "TLSA" "TXT") - -# Global variables -NEWLINE=$'\n' -TAB=$'\t' - -NAMED_DEFAULT_VIEW="_default" - - -######################################## -# bash array and JSON helper functions # -######################################## - -in_array() { - local search=$1 - shift - - local value - for value in "$@"; do - [ "$search" == "$value" ] && return 0 - done - - return 1 -} - -join_by() { - local d=${1-} f=${2-} - - if shift 2; then - printf %s "$f" "${@/#/$d}" - fi -} - -raw_to_json_obj() { - local delim=$1 - shift || return 1 - - local fields=() - local name index - while (( $# > 0 )); do - IFS=":" read -r name index <<<"$1" - shift - fields+=("$name:.[$index]") - done - - local jq_cmd=". | split(\$delim) | {$(join_by "," "${fields[@]}")}" - - "$JQ" --raw-input --compact-output --arg delim "$delim" "$jq_cmd" < <(cat -) - return $? -} - -json_array_to_bash() { - local retvar=${1:-REPLY} - - local _values=() - while IFS=$NEWLINE read -r line; do - _values+=("$("$JQ" --raw-output --compact-output '.[] | @sh' <<<"$line")") - done < <(cat -) - - declare -g -a $retvar - eval "$retvar=(${_values[@]})" -} - -bash_array_to_json() { - "$JQ" --raw-input --compact-output '.' < <(cat -) | "$JQ" --compact-output --slurp -} - -############################# -# internal helper functions # -############################# - -_get_keyfile() { - local retvar=$1 - local zone=$2 - local view=$3 - - local key_id - for key_id in "$zone@$view" "$view"; do - keyfile=${DNS_KEYS["$key_id"]} - if [ -n "$keyfile" ]; then - if ! test -r "$keyfile" -a -f "$keyfile"; then - echo "ERROR: key file does not exist or is not accessible -- '$keyfile'" >&2 - return 1 - fi - declare -g $retvar="$keyfile" - return 0 - fi - done - - if [ "$view" == "$NAMED_DEFAULT_VIEW" ]; then - declare -g $retvar="" - return 0 - fi - - echo "ERROR: no key configured for '$zone@$view' or '$view'" >&2 - return 1 -} - -_nsupdate() { - local zone=$1 - shift || return 1 - local view=$1 - shift || return 1 - local update_script=$1 - shift || return 1 - local pretend=${1:-true} - - _get_keyfile keyfile "$zone" "$view" || return $? - - local nsupdate_args="" - [ -n "$keyfile" ] && nsupdate_args="-k ${keyfile}" - - "$NSUPDATE" -v $nsupdate_args < <( - echo "server $DNS_IP" - echo "$update_script" - if $pretend; then - echo "show" - else - echo "send" - echo "answer" - fi - ) - - return $? -} - - -##################### -# general functions # -##################### - -dns_get_base_config() { - local view=$1 - local zone_dir_retvar=${2:-REPLY} - local conf_dir_retvar=${3:-REPLY} - local conf_file_retvar=${4:-REPLY} - - local base_config=${BASE_CONFIG[$view]} - [ -z "$base_config" ] && echo "ERROR: no base config found for view -- '$view'" >&2 && return 1 - - local zone_dir conf_dir conf_file - IFS=":" read -r zone_dir conf_dir conf_file <<<"$base_config" - [ -z "$zone_dir" -o -z "$conf_dir" -o -z "$conf_file" ] && echo "ERROR: invalid BASE_CONFIG for view -- '$view'" >&2 && return 2 - ! [ -d "$conf_dir" ] && echo "ERROR: conf dir: no such directory -- '$conf_dir'" >&2 && return 3 - ! [ -d "$zone_dir" ] && echo "ERROR: zone dir: no such directory -- '$zone_dir'" >&2 && return 4 - - declare -g $zone_dir_retvar="$zone_dir" - declare -g $conf_dir_retvar="$conf_dir" - declare -g $conf_file_retvar="$conf_file" -} - - -############################# -# data generation functions # -############################# - -_DNS_ZONES_DATA="" - -dns_zones() { - local zone=$1 - - if [ -z "$_DNS_ZONES_DATA" ]; then - local raw_data=$("$NAMED_CHECKCONF" -l | grep -P -i '^[^.][^ ]* IN ' | sort) || return 1 - local json_data=$(raw_to_json_obj " " "zone:0" "view:2" "status:3" <<<"$raw_data") || return 2 - declare -g _DNS_ZONES_DATA=$("$JQ" --compact-output --slurp 'sort_by(.zone)' <<<"$json_data") || return 3 - fi - - [ -z "$zone" ] && echo "$_DNS_ZONES_DATA" && return 0 - - "$JQ" --compact-output --arg zone "$zone" '[ .[] | select(.zone == $zone) ]' <<<"$_DNS_ZONES_DATA" -} - -dns_zone_views() { - local zone=$1 - - local zones=$(dns_zones "$zone") || return $? - "$JQ" --compact-output '[ .[] | .view ] | sort | unique' <<<"$zones" - return $? -} - -dns_zone() { - local zone=$1 - local view=$2 - local all=${3:-false} - - local keyfile - _get_keyfile keyfile "$zone" "$view" || return $? - - local dig_args - [ -n "$keyfile" ] && dig_args="-k ${keyfile}" - local raw_data=$("$DIG" @${DNS_IP} $dig_args +nocmd +onesoa +nostats AXFR "$zone" | \ - sed -r 's#^([^ \t]+)[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+(.+)#\1\t\2\t\3\t\4\t\5#g' | \ - grep -P -i '^[^\t]+\t[0-9]+\tIN\t') - - raw_data=$( - local name ttl _type rtype value - while read -r name ttl _type rtype value; do - if [ "${rtype^^}" == "TXT" ]; then - decoded_value=$(dns_decode_txt_value "$value") - else - decoded_value=$value - fi - printf "%s\t" "$name" "$ttl" "$rtype" "$value" "$decoded_value" - echo - done <<<"$raw_data" - ) - - local query='.' - local rtypes="[]" - - if ! $all; then - query='[ .[] | select(.type | IN($rtypes[])) ]' - rtypes=$(printf "%s\n" "${DNS_RECORD_TYPES[@]}" | bash_array_to_json) - fi - - query+='| [ .[] | .view += "\($view)" | if .name == "\($zone)." then .name = "@" else .name = "\(.name | capture("(?.+)\\.\($zone)").name )" end ] | sort_by(.name)' - - raw_to_json_obj "$TAB" "name:0" "ttl:1" "type:2" "value:3" "decoded_value:4" <<<"$raw_data" | \ - jq --compact-output --slurp --arg view "$view" --argjson rtypes "$rtypes" --arg zone "$zone" "$query" -} - - -############################## -# input validation functions # -############################## - -dns_check_zone() { - local zone=$1 - shift || return 1 - local retvar=${1:-REPLY} - - zone=$("$IDN2" <<<"$zone") - - local zones=$(dns_zones "$zone") || return 1 - - local query='if . | length > 0 then "true" else "false" end' - local found=$("$JQ" --raw-output --arg zone "$zone" "$query" <<<"$zones") - - $found && declare -g $retvar="$zone" && return 0 - - echo "ERROR: zone does not exist -- '$zone'" >&2 - return 2 -} - -dns_check_view() { - local view=$1 - shift || return 1 - - local query='. as $list | $view | IN($list[])' - $("$JQ" --raw-output --arg view "$view" "$query" < <(dns_zone_views)) && return 0 - - echo "ERROR: view does not exist -- '$view'" >&2 - return 1 -} - -dns_check_zone_view() { - local zone_view=$1 - local zone_retvar=$2 - local view_retvar=$3 - - local zone view - IFS='@' read -r zone view <<<"$zone_view" - - dns_check_zone "$zone" zone || return 1 - - local -a views - json_array_to_bash views < <(dns_zone_views "$zone") - - if [ -z "$view" ]; then - if (( ${#views[@]} > 1 )); then - echo "ERROR: zone is member of multiple views -- '$zone'" >&2 - return 2 - fi - if [ "${views[0]}" != "$NAMED_DEFAULT_VIEW" ]; then - echo "ERROR: zone is not a member of the default view -- '$zone'" >&2 - return 3 - fi - view=$NAMED_DEFAULT_VIEW - elif [ "${view}" != "*" ]; then - dns_check_view "$view" || return 4 - if ! in_array "$view" "${views[@]}"; then - echo "ERROR: zone '$zone' is not part of view '$view'" >&2 - return 5 - fi - fi - - [ -n "$zone_retvar" ] && declare -g $zone_retvar="$zone" - [ -n "$view_retvar" ] && declare -g $view_retvar="$view" - - return 0 -} - -dns_check_zone_name() { - local zone=${1,,} - local retvar=${2:-REPLY} - - zone=$("$IDN2" <<<"$zone") - if ! [[ "$zone" =~ ^[a-z0-9_][a-z0-9_.-]*$ ]] || [[ "$zone" == *"." ]]; then - echo "ERROR: invalid zone name -- '$zone'" >&2 - return 1 - fi - - declare -g $retvar="$zone" -} - -dns_check_zone_name_view() { - local zone_view=${1,,} - local zone_retvar=$2 - local view_retvar=$3 - - local zone view - IFS='@' read -r zone view <<<"$zone_view" - - dns_check_zone_name "$zone" zone || return 1 - - if [ -z "$view" ]; then - view=$NAMED_DEFAULT_VIEW - elif [ "$view" != "*" ]; then - dns_check_view "$view" || return 2 - fi - - local -a views=() - json_array_to_bash views < <(dns_zone_views "$zone") - - if (( ${#views[@]} > 0 )); then - if [ "${view}" == "*" ]; then - echo "ERROR: zone '$zone' already exists in these views -- '$(join_by ", " "${views[@]}")'" >&2 && return 4 - else - in_array "$view" "${views[@]}" && echo "ERROR: zone '$zone' already exists in view -- '$view'" >&2 && return 5 - fi - fi - - [ -n "$zone_retvar" ] && declare -g $zone_retvar="$zone" - [ -n "$view_retvar" ] && declare -g $view_retvar="$view" - - return 0 -} - -dns_check_record_name() { - local name=${1,,} - local retvar=${2:-REPLY} - - [ "$name" == "@" ] && return 0 - - name=$("$IDN2" <<<"$name") - - local LC_ALL=C - if [[ "$name" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$ ]] && [[ "$name" != *"." ]]; then - declare -g $retvar="$name" - return 0 - fi - - echo "ERROR: invalid record name -- '$name'" >&2 - return 1 -} - -dns_check_ttl() { - local ttl=$1 - - [[ "$ttl" =~ ^[1-9][0-9]*$ ]] && (( $ttl <= 2147483647 )) && return 0 - - echo "ERROR: invalid TTL -- '$ttl'" >&2 - return 1 -} - -dns_check_record_type() { - local rtype=$1 - - [ -z "$rtype" ] && return 1 - - if ! in_array "${rtype^^}" "${DNS_RECORD_TYPES[@]}"; then - echo "ERROR: invalid record type -- '$rtype'" >&2 - return 1 - fi - - return 0 -} - -dns_check_record_value() { - local rtype=$1 - local value=$2 - local retvar=${3:-REPLY} - - [ -z "$rtype" ] && return 1 - [ -z "$value" ] && return 1 - - case "${rtype^^}" in - A) - local digit_re='(0|1[0-9]{0,2}|2([0-9]|[0-4][0-9]|5[0-5])?)' - local ipv4_re="^$digit_re\.$digit_re\.$digit_re\.$digit_re\$" - local LC_ALL=C - if ! [[ "$value" =~ $ipv4_re ]]; then - echo "ERROR: invalid IPv4 address -- '$value'" 2>&1 - return 1 - fi - ;; - AAAA) - local ipv6_re='^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$' - local LC_ALL=C - if ! [[ "$value" =~ $ipv6_re ]]; then - echo "ERROR: invalid IPv6 address -- '$value'" 2>&1 - return 1 - fi - ;; - CNAME|PTR|NS) - dns_check_zone_name "$value" || return 1 - ;; - MX) - local prio mx - read -r prio mx <<<"$value" - [ -z "$rio" -a -z "$mx" ] && echo "ERROR: invalid MX record format -- '$value'" >&2 && return 1 - local LC_ALL=C - ! [[ "$prio" =~ ^[1-9][0-9]*$ ]] && echo "ERROR: invalid priority -- '$prio'" >&2 && return 1 - dns_check_zone_name "$mx" || return 1 - ;; - TXT) - if [ "${value:0:1}" != '"' ]; then - local -a parts=() - local index=0 - while (( ${#value} - $index > 0 )); do - parts+=("${value:$index:255}") - ((index+=255)) - done - value="\"$(join_by '" "' "${parts[@]}")\"" - fi - ;; - esac - - declare -g $retvar="$value" - - return 0 -} - - -######################## -# user input functions # -######################## - -yes_no() { - local yn="" - - while :; do - read -p "$* (yes/no): " yn - case "$yn" in - [Yy]*) return 0 ;; - [Nn]*) return 1 ;; - esac - done -} - -dns_select_zone() { - local zone_retvar=$1 - local view_retvar=$2 - - local -a zones=() - - local zones=$(dns_zones) || return $? - - json_array_to_bash zones < <("$JQ" --compact-output '[ .[] | .zone ] | unique' <<<"$zones") - (( ${#zones[@]} == 0 )) && echo "ERROR: no zones found" >&2 && return 1 - - local COLUMNS=30 - - echo -e "Select zone:\n" - select zone in "${zones[@]}"; do - [ -n "$zone" ] && break - done - echo - [ -z "$zone" ] && echo "ERROR: zone selection failed" >&2 && return 1 - - local views=() - json_array_to_bash views < <(dns_zone_views "$zone") - - local view="" - if (( ${#views[@]} == 1 )) && [ "${views[0]}" == "$NAMED_DEFAULT_VIEW" ]; then - view=$NAMED_DEFAULT_VIEW - else - echo -e "Select view:\n" - select view in "${views[@]}"; do - [ -n "$view" ] && break - done - echo - [ -z "$view" ] && echo "ERROR: view selection failed" >&2 && return 1 - fi - - declare -g $zone_retvar="$zone" - declare -g $view_retvar="$view" -} - -dns_select_record_type() { - local retvar=${1:-REPLY} - - echo "Select record type:" - local rtype - select rtype in "${DNS_RECORD_TYPES[@]}"; do - [ -n "$rtype" ] && break - done - echo - [ -z "$rtype" ] && echo "ERROR: record type selection failed" >&2 && return 1 - - declare -g $retvar="$rtype" -} - -dns_read_zone_view() { - local zone_retvar=$1 - local view_retvar=$2 - - local zone - while [ -z "$zone" ]; do - read -e -p "Zone name (ZONE or ZONE@VIEW): " zone - [ -n "$zone" ] && ! dns_check_zone_name_view "$zone" "$zone_retvar" "$view_retvar" && zone="" - done - echo -} - -dns_read_record_name() { - local retvar=${1:-REPLY} - - local name="" - while [ -z "$name" ]; do - read -e -p "Record name (e.g. www or mail): " name - [ -n "$name" ] && ! dns_check_record_name "$name" name && name="" - done - echo - - declare -g $retvar="$name" -} - -dns_read_ttl() { - local retvar=${1:-REPLY} - - local ttl - while [ -z "$ttl" ]; do - read -e -p "TTL (seconds): " -i 7200 ttl - [ -n "$ttl" ] && ! dns_check_ttl "$ttl" && ttl="" - done - echo - - declare -g $retvar="$ttl" -} - -dns_read_record_value() { - local rtype=$1 - local retvar=${2:-REPLY} - - local value - while [ -z "$value" ]; do - read -e -p "Value: " value - [ -n "$value" ] && ! dns_check_record_value "$rtype" "$value" value && value="" - done - echo - declare -g $retvar="$value" -} - -dns_decode_txt_value() { - local value=$1 - [ "${value:0:1}" != '"' ] && echo "$value" && return - - value=${value#\"} - value=${value%\"} - sed -r 's#([^\])" "#\1#g;s#\\"#"#g' <<<"$value" -} - - -#################### -# action functions # -#################### - -dns_record_add() { - local pretend=$1 - local zone=$2 - local view=$3 - local name=$4 - local ttl=$5 - local rtype=$6 - local value=$7 - - [ -z "$pretend" ] && echo "ERROR: missing argument -- PRETEND" >&2 && return 1 - ! in_array "$pretend" "true" "false" && echo "ERROR: invalid argument PRETEND -- '$pretend'" >&2 && return 1 - [ -z "$zone" ] && echo "ERROR: missing argument -- ZONE" >&2 && return 1 - [ -z "$view" ] && echo "ERROR: missing argument -- VIEW" >&2 && return 1 - [ -z "$name" ] && echo "ERROR: missing argument -- NAME" >&2 && return 1 - [ -z "$ttl" ] && echo "ERROR: missing argument -- TTL" >&2 && return 1 - [ -z "$rtype" ] && echo "ERROR: missing argument -- TYPE" >&2 && return 1 - [ -z "$value" ] && echo "ERROR: missing argument -- VALUE" >&2 && return 1 - - local fqdn - [ "$name" == "@" ] && fqdn=$zone || fqdn="$name.$zone" - - update_script="" - if [ "${rtype^^}" == "CNAME" ]; then - update_script="prereq nxdomain $fqdn$NEWLINE" - fi - update_script+="update add $fqdn $ttl $rtype $value" - - _nsupdate "$zone" "$view" "$update_script" "$pretend" - return $? -} - -dns_record_delete() { - local pretend=$1 - local zone=$2 - local view=$3 - local name=$4 - local rtype=$5 - local value=$6 - - [ -z "$pretend" ] && echo "ERROR: missing argument -- PRETEND" >&2 && return 1 - ! in_array "$pretend" "true" "false" && echo "ERROR: invalid argument PRETEND -- '$pretend'" >&2 && return 1 - [ -z "$zone" ] && echo "ERROR: missing argument -- ZONE" >&2 && return 1 - [ -z "$view" ] && echo "ERROR: missing argument -- VIEW" >&2 && return 1 - [ -z "$name" ] && echo "ERROR: missing argument -- NAME" >&2 && return 1 - [ -z "$rtype" ] && echo "ERROR: missing argument -- TYPE" >&2 && return 1 - [ -z "$value" ] && echo "ERROR: missing argument -- VALUE" >&2 && return 1 - - local fqdn - [ "$name" == "@" ] && fqdn=$zone || fqdn="$name.$zone" - [ -n "$value" ] && value=" $value" - - update_script="update delete $fqdn $rtype$value" - - _nsupdate "$zone" "$view" "$update_script" "$pretend" - return $? -} - -dns_reload_config() { - echo -n "Reload Bind config... " - rndc_args="" - [ -n "$CONTROL_KEY" ] && rndc_args="-k $CONTROL_KEY" - ! "$RNDC" $rndc_args reconfig && echo "ERROR: rndc reconfig failed" >&2 && return 1 - echo "Ok" -} diff --git a/lib/output.sh b/lib/output.sh deleted file mode 100644 index 57de162..0000000 --- a/lib/output.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -_prettytable_char_top_left="┌" -_prettytable_char_horizontal="─" -_prettytable_char_vertical="│" -_prettytable_char_bottom_left="└" -_prettytable_char_bottom_right="┘" -_prettytable_char_top_right="┐" -_prettytable_char_vertical_horizontal_left="├" -_prettytable_char_vertical_horizontal_right="┤" -_prettytable_char_vertical_horizontal_top="┬" -_prettytable_char_vertical_horizontal_bottom="┴" -_prettytable_char_vertical_horizontal="┼" - - -# Escape codes - -# Default colors -_prettytable_color_blue="0;34" -_prettytable_color_green="0;32" -_prettytable_color_cyan="0;36" -_prettytable_color_red="0;31" -_prettytable_color_purple="0;35" -_prettytable_color_yellow="0;33" -_prettytable_color_gray="1;30" -_prettytable_color_light_blue="1;34" -_prettytable_color_light_green="1;32" -_prettytable_color_light_cyan="1;36" -_prettytable_color_light_red="1;31" -_prettytable_color_light_purple="1;35" -_prettytable_color_light_yellow="1;33" -_prettytable_color_light_gray="0;37" - -# Somewhat special colors -_prettytable_color_black="0;30" -_prettytable_color_white="1;37" -_prettytable_color_none="0" - -function _prettytable_prettify_lines() { - cat - | sed -e "s@^@${_prettytable_char_vertical}@;s@\$@ @;s@ @ ${_prettytable_char_vertical}@g" -} - -function _prettytable_fix_border_lines() { - cat - | sed -e "1s@ @${_prettytable_char_horizontal}@g;3s@ @${_prettytable_char_horizontal}@g;\$s@ @${_prettytable_char_horizontal}@g" -} - -function _prettytable_colorize_lines() { - local color="$1" - local range="$2" - local ansicolor="$(eval "echo \${_prettytable_color_${color}}")" - - cat - | sed -e "${range}s@\\([^${_prettytable_char_vertical}]\\{1,\\}\\)@"$'\E'"[${ansicolor}m\1"$'\E'"[${_prettytable_color_none}m@g" -} - -function prettytable() { - local cols="${1}" - local color="${2:-none}" - local input="$(cat -)" - local header="$(echo -e "${input}"|head -n1)" - local body="$(echo -e "${input}"|tail -n+2)" - { - # Top border - echo -n "${_prettytable_char_top_left}" - for i in $(seq 2 ${cols}); do - echo -ne "\t${_prettytable_char_vertical_horizontal_top}" - done - echo -e "\t${_prettytable_char_top_right}" - - echo -e "${header}" | _prettytable_prettify_lines - - # Header/Body delimiter - echo -n "${_prettytable_char_vertical_horizontal_left}" - for i in $(seq 2 ${cols}); do - echo -ne "\t${_prettytable_char_vertical_horizontal}" - done - echo -e "\t${_prettytable_char_vertical_horizontal_right}" - - echo -e "${body}" | _prettytable_prettify_lines - - # Bottom border - echo -n "${_prettytable_char_bottom_left}" - for i in $(seq 2 ${cols}); do - echo -ne "\t${_prettytable_char_vertical_horizontal_bottom}" - done - echo -e "\t${_prettytable_char_bottom_right}" - } | column -t -s $'\t' | _prettytable_fix_border_lines | _prettytable_colorize_lines "${color}" "2" -} - -table_output() { - local n_cols=$# - { - printf "$(join_by "\t" "$@")\n" - cat - - } | prettytable "$n_cols" "${TABLE_HEADER_COLOR:-gray}" -}