#!/bin/bash
#
# Bash library and executable that wraps 'logger' to write log on stderr or syslog,
# then exit if log level is more serious than BL_LOGEXITLNAME.
#
# Copyright © 2021 Jean-Jacques Brucker <jjbrucker@free.fr>
#
# SPDX-License-Identifier: LGPL-3.0-only


if [[ "$1" == --bash-completion ]] ; then
	_BL_LOG_LEVELS_NAMES="emerg alert crit err error warning notice info debug"
	_BL_LOG_LEVELS_REGEX=$(awk '{ OFS="\\|"; NF=NF; print "\\<\\("$0"\\)\\>" }' <<< "$_BL_LOG_LEVELS_NAMES")
	_BL_LOG_LEVELS_LIST=$(awk  '{ OFS=",";   NF=NF; print      "{"$0"}" }'      <<< "$_BL_LOG_LEVELS_NAMES")
	readonly _BL_LOG_LEVELS_NAMES _BL_LOG_LEVELS_REGEX _BL_LOG_LEVELS_LIST

	_BL_LOG_SYSLOG_PORT=$(awk '$1 ~ /^syslog$/  {split($2, a, "/"); print a[1]}' /etc/services)
	_BL_LOG_SOCKET_LIST=$(awk '$NF ~ /^\// {print $NF}'                          /proc/net/unix)
	readonly _BL_LOG_SYSLOG_PORT  _BL_LOG_SOCKET_LIST

	_BL_LOG_OPTIONS="--help --version --msgid --id= --log-level --log-exit"
	_BL_LOG_OPTIONS="$_BL_LOG_OPTIONS --prio-prefix --rfc3164 --rfc5424="
	_BL_LOG_OPTIONS="$_BL_LOG_OPTIONS --server --port --tcp --udp --socket"
	_BL_LOG_OPTIONS="$_BL_LOG_OPTIONS --socket-errors --socket-errors="
	_BL_LOG_OPTIONS="$_BL_LOG_OPTIONS --size --no-act --octet-count --quiet"
	_BL_LOG_OPTIONS="$_BL_LOG_OPTIONS --stderr --file --tag --skip-empty"
	_BL_LOG_OPTIONS="$_BL_LOG_OPTIONS --journald --journald= --color --color="
	_BL_LOG_FACILITIES="{auth,authpriv,cron,daemon,ftp,lpr,mail,news,syslog}"
	readonly _BL_LOG_OPTIONS _BL_LOG_FACILITIES

	_bl_log_compgen ()
	{
		mapfile -t COMPREPLY < <(compgen "$@" || true)
	}

	_bl_log_completion()
	{
		local cur
		COMPREPLY=()
		cur="${COMP_WORDS[COMP_CWORD]}"
		grep -q -- "$_BL_LOG_LEVELS_REGEX" <<< "${COMP_WORDS[@]}" ||
		case ${COMP_WORDS[COMP_CWORD-1]} in
			'-h'|'--help'|'-V'|'--version')                                     ;;
			'-t'|'--tag')    _bl_log_compgen -W "tag"                  -- "$cur";;
			'--msgid')       _bl_log_compgen -W "msgid"                -- "$cur";;
			'-n'|'--server') _bl_log_compgen -A hostname               -- "$cur";;
			'-P'|'--port')   _bl_log_compgen -W "$_BL_LOG_SYSLOG_PORT" -- "$cur";;
			'-u'|'--socket') _bl_log_compgen -W "$_BL_LOG_SOCKET_LIST" -- "$cur";;
			'--log-exit')    _bl_log_compgen -W "$_BL_LOG_LEVELS_LIST" -- "$cur";;
			'-f'|'--file')   local IFS=$'\n'; compopt -o filenames; _bl_log_compgen -f -- "$cur";;
			*)
				case $cur in
					'--journald'*)                 local IFS=$'\n'; compopt -o filenames; _bl_log_compgen -P "--journald=" -f -- "${cur:12}";;
					'--socket-errors'*|'--color'*) _bl_log_compgen -W "--{socket-errors,color}={on,off,auto}"    -- "$cur";;
					-*)                            _bl_log_compgen -W "${_BL_LOG_OPTIONS}"                       -- "$cur";;
					*)                             _bl_log_compgen -W "$_BL_LOG_FACILITIES.$_BL_LOG_LEVELS_LIST" -- "$cur";;
				esac;;
		esac
	}

	complete -F _bl_log_completion "$(basename "${BASH_SOURCE[0]}")" "${BASH_SOURCE[0]}"
	return 0
fi

# If sourcing while _bl_log_parseoptions is already set, execute the function and return without reloading rest of file.
if [[ "${BASH_SOURCE[0]}" != "$0" ]] && [[ "$(type -t _bl_log_parseoptions || true)" == function ]]
then
	_BL_LOG_RETVAL=0
	_bl_log_parseoptions "$BL_LOG_NAME" "$@" || _BL_LOG_RETVAL=$?
	return "${_BL_LOG_RETVAL}"
fi

### Constants ###

BL_LOG_NAME="$(basename "$(readlink -f "${BASH_SOURCE[0]}" || true)" )"
BL_LOG_VERSION="0.3.5-1"
readonly BL_LOG_NAME BL_LOG_VERSION

declare -Ar BL_LOGLEVELS=(
[emerg]=0
[alert]=1
[crit]=2
[error]=3 # deprecated synonym for err
[err]=3
[warning]=4
[notice]=5
[info]=6
[debug]=7)

### Others Globals ###

BL_LOGCOLORS=(47 45 41 31 35 33 36 37)
BL_LOGLEVEL="${BL_LOGLEVEL:-7}"
BL_LOG_O_STDERR="--stderr"
BL_LOGGER_OPTIONS=""
BL_LOG_COLOR="auto"
BL_LOGEXITLNAME="${BL_LOGEXITLNAME:-emerg}"

if [[ "${BASH_SOURCE[0]}" == "$0" ]] ; then
	# run as a program

	TEXTDOMAIN="bashlibs"
	TEXTDOMAINDIR="$(dirname "$(readlink -f "$BASH_SOURCE")" )/../share/locale"
	# Require Bash 5.2. - https://www.kurokatta.org/grumble/2023/11/bash-translated-strings#fnref3
	shopt -s noexpand_translation

	BL_LOG_usage="Usage: $(basename "${BASH_SOURCE[0]}") [OPTIONS]... PRIORITY [MESSAGE]..."
	BL_LOG_shelpmsg="
$BL_LOG_NAME is also bash library, see:
$ source ${BASH_SOURCE[0]} --help"
else
	# run as a library (source $0)

	BL_LOG_usage="Usage: source ${BASH_SOURCE[0]} [OPTIONS]..."

	BL_LOG_shelpmsg="
      --bash-completion    set completion for ${BASH_SOURCE[0]} program and return (without loading anything else)

Reminder: when used as a library, all functions calls share the same environment variables,
i.e. the same options passed when loading the library."
fi

### Help messages ###

BL_LOG_chelpmsg="
"$"Wrapper for logger command, which also print pretty logs on stderr.""
"$"PRIORITY is a"" \"facility.level\" pair. "$"The default facility is [user]=1.""
"$"Levels:"" $(declare -p BL_LOGLEVELS)
"$"If PRIORITY's level is under"" BL_LOGLEVEL=$BL_LOGLEVEL, "$"don't log anything.""
"$"If PRIORITY's level name is more serious than"" BL_LOGEXITLNAME=$BL_LOGEXITLNAME, exit(8+'PRIORITY's level')
"$"If there is no MESSAGE in command line, read it from stdin.""

OPTIONS:
  -l, --log-level LEVEL    "$"log level:"" emerg<1=alert<crit<3=err<warning<5=notice<info<7=debug (current: $BL_LOGLEVEL)
  -L, --log-exit LEVELNAME "$"exit level:"" emerg|alert|crit|err|warning|... (current: $BL_LOGEXITLNAME )
  -q, --quiet              "$"do not output message to standard error""
  -Q, --no-act             "$"do not send message to the logs system (syslog)""
      --color[=<on|off|auto>]
                           "$"colorize messages sent to standard error (default: auto)""
  -h, --help               "$"Show help and exit/return""
  -V, --version            "$"Show version and exit/return""

"$"Options forwarded to 'logger':""
  -i                       "$"log the logger command's PID""
      --id[=<id>]          "$"log the given <id>, or otherwise the PID"" (default \$\$ = logger's PPID)
  -f, --file <file>        "$"log the contents of this file""
  -e, --skip-empty         "$"do not log empty lines when processing files""
      --octet-count        "$"use rfc6587 octet counting""
      --prio-prefix        "$"look for a prefix on every line read from stdin""
  -S, --size <size>        "$"maximum size for a single message""
  -t, --tag <tag>          "$"mark every line with this tag"" (default \${0##*/})
  -n, --server <name>      "$"write to this remote syslog server""
  -P, --port <port>        "$"use this port for UDP or TCP connection""
  -T, --tcp                "$"use TCP only""
  -d, --udp                "$"use UDP only""
      --rfc3164            "$"use the obsolete BSD syslog protocol""
      --rfc5424[=<snip>]   "$"use the syslog protocol (the default for remote);""
                             <snip> "$"can be notime, or notq, and/or nohost""
      --sd-id <id>         "$"rfc5424 structured data ID""
      --sd-param <data>    "$"rfc5424 structured data name=value""
      --msgid <msgid>      "$"set rfc5424 message id field""
  -u, --socket <socket>    "$"write to this Unix socket""
      --socket-errors[=<on|off|auto>]
                           "$"print connection errors when using Unix sockets""
      --journald[=<file>]  "$"write journald entry"""


### functions ###

_bl_log_print_help ()
{
	echo "$BL_LOG_usage"
	echo "$BL_LOG_chelpmsg"
	echo "$BL_LOG_shelpmsg"
}

_bl_log_print_version ()
{
	local name="$1"
	printf "%s %s\n" "$name" "$BL_LOG_VERSION"
}

_bl_log_parse_exit ()
{
	BL_LOGEXITLNAME="$1"

	for item in "${!BL_LOGLEVELS[@]}"
	do
		if [[ ${BL_LOGLEVELS[$item]} == "${BL_LOGEXITLNAME}" ]]
		then
			BL_LOGEXITLNAME="${item}"
			break
		fi
	done

	if ! grep -q "\<$BL_LOGEXITLNAME\>" <<<"${!BL_LOGLEVELS[@]}"
	then
		echo "Error: log-exit '$1' is none of: ${!BL_LOGLEVELS[*]}" >&2
		_BL_LOG_RETVAL=2
	else
		_BL_LOG_RETVAL=0
	fi
}

_bl_log_parse_level ()
{
	BL_LOGLEVEL="$1";

	for item in "${!BL_LOGLEVELS[@]}"
	do
		if [[ "${BL_LOGLEVEL}" = "${item}" ]]
		then
			BL_LOGLEVEL="${BL_LOGLEVELS[${item}]}"
			break
		fi
	done

	if [[ "$BL_LOGLEVEL" != [0-7] ]]
	then
		echo "Error: log-level out of range [0-7]" >&2
		_BL_LOG_RETVAL=2
	else
		_BL_LOG_RETVAL=0
	fi
}

#TODO: explain in comment why we dont use getopts...
_bl_log_parseoptions() {
	local name=$1

	if [[ "$name" =~ ^- ]]
	then
		echo "Usage: ${FUNCNAME[0]} NAME" '"$@"'                                                                              >&2
		echo 'For program/function named NAME: parse Options in $@ and set BL_LOG_ARGV array containing remaining parameters' >&2
		return 1
	fi

	_bl_log_print_bad_option ()
	{
		echo "${name}: unrecognized option '$1'"          >&2
		echo                                              >&2
		echo "Try '${name} --help' for more information." >&2
	}

	shift

	for ((;$#;)) ; do
		case "$1" in
			--color|--color=on)
				BL_LOG_COLOR=on;;
			--color=off)
				BL_LOG_COLOR=off;;
			--color=auto)
				BL_LOG_COLOR=auto;;
			-q|--quiet)
				BL_LOG_O_STDERR="";;
			-Q|--no-act)
				BL_LOGGER_OPTIONS+=("--no-act");;
			-i|-e|--skip-empty|--octet-count|--prio-prefix|-T|--tcp|-d|--udp|--rfc3164)
				BL_LOGGER_OPTIONS+=("$1");;
			-f|--file|-S|--size|-t|--tag|-n|--server|-P|--port|--sd-id|--sd-param|--msgid|-u|--socket)
				BL_LOGGER_OPTIONS+=("$1"); shift; BL_LOGGER_OPTIONS+=("$1");;
			--id*|--rfc5424*|--socket-errors*|--journald*)
				BL_LOGGER_OPTIONS+=("$1");;
			-l|--log-l*)
				shift
				_bl_log_parse_level "$1"
				[[ "$_BL_LOG_RETVAL" == 0 ]] || return "$_BL_LOG_RETVAL";;
			-L|--log-e*)
				shift; _bl_log_parse_exit  "$1"
				[[ "$_BL_LOG_RETVAL" == 0 ]] || return "$_BL_LOG_RETVAL";;
			-h|--help)
				_bl_log_print_help
				return 1;;
			-V|--version)
				_bl_log_print_version "$name"
				return 1;;
			--)
				shift
				break;;
			-*)
				_bl_log_print_bad_option "$1"
				return 2;;
			*)
				break;;
		esac
		shift
	done
	BL_LOG_ARGV=("$@")
}


bl_log() {
	local BL_LOG_usage="Usage: ${FUNCNAME[0]} [OPTIONS]... PRIORITY [MESSAGE]..."
	local return_value=0

	_bl_log_parseoptions "${FUNCNAME[0]}" "$@" || return_value=$?

	[[ "$return_value" == 0 ]] || return "$return_value"

	if [[ -z "${BL_LOG_ARGV[*]}" ]]
	then
		echo "Error: Please give at least a priority (${!BL_LOGLEVELS[*]})" >&2
		return 2
	fi

	set "${BL_LOG_ARGV[@]}"

	local priority=$1
	local plname=${1##*.}
	local result

	for item in "${!BL_LOGLEVELS[@]}"
	do
		if [[ ${BL_LOGLEVELS[$item]} == "${plname}" ]]
		then
			plname="${item}"
			break
		fi
	done

	if ! grep -q "\<$plname\>" <<<"${!BL_LOGLEVELS[@]}"
	then
		echo "Error: priority's level name '$plname' is none of: ${!BL_LOGLEVELS[*]}" >&2
		return 2
	fi

	shift

	local llevel=${BL_LOGLEVELS[$plname]}

	if ((llevel > BL_LOGLEVEL))
	then
		# If there is no MESSAGE, purge stdin; else do nothing.
		[[ -n "$*" ]] || cat >/dev/null
	else
		if [[ "$BL_LOG_COLOR" == on ]] || [[ "$BL_LOG_COLOR" == auto ]] && [[ -t 2 ]]
		then
			local SET_COLOR="\x1b[${BL_LOGCOLORS[$llevel]}m"
			local RESET_COLOR="\x1b[0m"
		else
			local SET_COLOR=""
			local RESET_COLOR=""
		fi

		result=$(eval logger -p "$priority" "$BL_LOG_O_STDERR" "--id=$$" "--tag ${0##*/}" '${BL_LOGGER_OPTIONS[@]} -- "$@"' 2>&1)
		echo -e "${result/: /: ${SET_COLOR}${plname^}:${RESET_COLOR} }" 1>&2
	fi
	((llevel > BL_LOGLEVELS[$BL_LOGEXITLNAME] )) || exit $((168+llevel))
}


### Init ###

# Parse Options
_BL_LOG_RETVAL=0
_bl_log_parseoptions "$BL_LOG_NAME" "$@" || _BL_LOG_RETVAL="$?"

# Do nothing else if sourced
[[ "${BASH_SOURCE[0]}" == "$0" ]] || return 0

[[ "$_BL_LOG_RETVAL"   != "1"  ]] || exit 0
[[ "$_BL_LOG_RETVAL" < 2 ]] || exit $_BL_LOG_RETVAL

### Run ###
set -e

bl_log "${BL_LOG_ARGV[@]}"
