-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
377 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,377 @@ | ||
#! /bin/bash | ||
# | ||
# Standard logging | ||
# | ||
# Exports: | ||
# @go.log | ||
# Outputs a log line; supports terminal formatting and format stripping | ||
# | ||
# @go.log_command | ||
# Logs a command and its outcome | ||
# | ||
# @go.critical_section_begin | ||
# Causes @go.log_command to log FATAL on error | ||
# | ||
# @go.critical_section_end | ||
# Cause @go.log_command to log ERROR on error | ||
# | ||
# @go.add_or_update_log_level | ||
# Modifies existing log level labels, formatting, and file descriptors | ||
# | ||
# @go.setup_project | ||
# Runs the project's 'setup' script and logs the result | ||
# | ||
# See the function comments for each of the above for further information. | ||
|
||
# Set this if you want to force terminal-formatted output from @go.log. | ||
# | ||
# @go.log will remove terminal formatting codes by default when the output file | ||
# descriptor is not a terminal. Can be set either in the script or on the | ||
# command line. | ||
declare _GO_LOG_FORMATTING="$_GO_LOG_FORMATTING" | ||
|
||
# If not empty, @go.log_command will only print the command and not execute it. | ||
# | ||
# Can be set either in the script or on the command line. | ||
declare _GO_DRY_RUN="$_GO_DRY_RUN" | ||
|
||
# Default log level labels | ||
# | ||
# DO NOT UPDATE DIRECTLY: Use @go.add_or_update_log_level instead. | ||
declare _GO_LOG_LEVELS=( | ||
'INFO' | ||
'RUN' | ||
'WARN' | ||
'ERROR' | ||
'FATAL' | ||
'START' | ||
'FINISH' | ||
) | ||
|
||
# Default log level terminal format codes | ||
declare __GO_LOG_LEVELS_FORMAT_CODES=( | ||
'\e[1m\e[36m' | ||
'\e[1m\e[35m' | ||
'\e[1m\e[33m' | ||
'\e[1m\e[31m' | ||
'\e[1m\e[31m' | ||
'\e[1m\e[32m' | ||
'\e[1m\e[32m' | ||
) | ||
|
||
# Default log level output file descriptors | ||
# | ||
# '1' is standard output and '2' is standard error. | ||
declare __GO_LOG_LEVELS_FILE_DESCRIPTORS=( | ||
'1' | ||
'1' | ||
'1' | ||
'2' | ||
'2' | ||
'1' | ||
'1' | ||
) | ||
|
||
# DO NOT EDIT: Initialized by @go.log | ||
declare __GO_LOG_LEVELS_FORMATTED=() | ||
|
||
# Set by @go.critical_section_{begin,end} | ||
declare _GO_CRITICAL_SECTION= | ||
|
||
# Outputs a single log line that may contain terminal control characters. | ||
# | ||
# Usage: | ||
# | ||
# @go.log <log-level> args... | ||
# @go.log <ERROR|FATAL> <exit-status|''> args... | ||
# | ||
# Where: | ||
# | ||
# <log-level> A label from _GO_LOG_LEVELS | ||
# <exit-status> The exit status number to return from an ERROR or FATAL call | ||
# args... Arguments comprising the log record text | ||
# | ||
# Will automatically format the '<log-level>' label if writing to the terminal | ||
# or _GO_LOG_FORMATTING is set. Will automatically strip format codes from the | ||
# remaining arguments if not writing to the terminal and _GO_LOG_FORMATTING is | ||
# empty. | ||
# | ||
# If the first argument is ERROR or FATAL, the second argument is the exit | ||
# status, and the remainder of the arguments comprise the log record. The exit | ||
# status will be appended to the log record if it is not the empty string. | ||
# | ||
# ERROR will cause @go.log to return the exit status; FATAL will exit the | ||
# process with the exit status. If the exit status is the empty string, it will | ||
# default to 1. | ||
# | ||
# If you want to add a custom log level, or change an existing log level, do so | ||
# using @go.add_or_update_log_level before the first call to @go.log, most | ||
# likely in your ./go script. | ||
# | ||
# Arguments: | ||
# $1: log level label; will be converted to all-uppercase | ||
# $2: exit status if $1 is ERROR or FATAL; first log record element otherwise | ||
# $3..$#: remainder of the log record | ||
@go.log() { | ||
local args=("$@") | ||
local log_level="${args[0]^^}" | ||
local formatted_log_level | ||
local level_fd=1 | ||
local exit_status=0 | ||
local close_code='\e[0m' | ||
local echo_mode='-e' | ||
|
||
unset 'args[0]' | ||
_@go.log_init | ||
|
||
local __go_log_level_index=0 | ||
if ! _@go.log_level_index "$log_level"; then | ||
@go.log ERROR '' "Unknown log level $log_level; defaulting to WARN" | ||
@go.log WARN "${args[@]}" | ||
return 1 | ||
fi | ||
|
||
formatted_log_level="${__GO_LOG_LEVELS_FORMATTED[$__go_log_level_index]}" | ||
level_fd="${__GO_LOG_LEVELS_FILE_DESCRIPTORS[$__go_log_level_index]}" | ||
|
||
if [[ "$log_level" =~ ERROR|FATAL ]]; then | ||
exit_status="${args[1]}" | ||
unset 'args[1]' | ||
|
||
if [[ -n "$exit_status" ]]; then | ||
args+=("(exit status $exit_status)") | ||
else | ||
exit_status=1 | ||
fi | ||
fi | ||
|
||
if [[ ! -t "$level_fd" && -z "$_GO_LOG_FORMATTING" ]]; then | ||
echo_mode='-E' | ||
args=("${args[@]//\\e\[[0-9]m}") | ||
args=("${args[@]//\\e\[[0-9][0-9]m}") | ||
args=("${args[@]//\\e\[[0-9][0-9][0-9]m}") | ||
close_code='' | ||
fi | ||
|
||
echo "$echo_mode" "$formatted_log_level ${args[*]}$close_code" >&"$level_fd" | ||
|
||
if [[ "$log_level" == FATAL ]]; then | ||
exit "$exit_status" | ||
fi | ||
return "$exit_status" | ||
} | ||
|
||
# Sets @go.log_command to log FATAL when its command exits with an error status. | ||
@go.critical_section_begin() { | ||
_GO_CRITICAL_SECTION='true' | ||
} | ||
|
||
# Sets @go.log_command to log ERROR when its command exits with an error status. | ||
@go.critical_section_end() { | ||
_GO_CRITICAL_SECTION= | ||
} | ||
|
||
# Logs the specified command and its outcome. | ||
# | ||
# By default it will log ERROR and return the command's exit status on failure. | ||
# In between calls to @go.critical_section_begin and @go.critical_section_end, | ||
# it will instead log FATAL and exit the process with the command's status code. | ||
# | ||
# Arguments: | ||
# The command and its arguments to log and execute | ||
@go.log_command() { | ||
local args=("$@") | ||
local cmd_string="${args[*]}" | ||
local err_msg | ||
local exit_status | ||
|
||
if [[ "${args[0]}" == '@go' ]]; then | ||
cmd_string="$_GO_CMD ${args[*]:1}" | ||
fi | ||
@go.log RUN "$cmd_string" | ||
|
||
if [[ -n "$_GO_DRY_RUN" ]]; then | ||
return | ||
fi | ||
|
||
"${args[@]}" | ||
exit_status="$?" | ||
|
||
if [[ "$exit_status" -ne '0' ]]; then | ||
if [[ -n "$_GO_CRITICAL_SECTION" ]]; then | ||
@go.log FATAL "$exit_status" "$err_msg" | ||
fi | ||
@go.log ERROR "$exit_status" "$err_msg" | ||
return "$exit_status" | ||
fi | ||
} | ||
|
||
# See @go.add_or_update_log_level in go-core.bash for description. | ||
# Adds a new log level or updates an existing one. | ||
# | ||
# If you wish to keep the existing format code or file descriptor, specify | ||
# 'keep' as the second argument or third argument, respectively. | ||
# | ||
# Arguments: | ||
# $1: The log level label; will be converted to all-uppercase | ||
# $2: The terminal format code that should precede the label | ||
# $3: The file descriptor to which to output level messages (defaults to 1) | ||
@go.add_or_update_log_level() { | ||
local log_level="${1^^}" | ||
local format_code="$2" | ||
local level_fd="$3" | ||
if [[ -z "$level_fd" ]]; then | ||
level_fd=1 | ||
fi | ||
|
||
if [[ -n "$__GO_LOG_INIT" ]]; then | ||
@go.log 'FATAL' '' "Can't set logging level $log_level; already intialized" | ||
fi | ||
|
||
local __go_log_level_index=0 | ||
if ! _@go.log_level_index "$log_level"; then | ||
if [[ "$format_code" == 'keep' || "$level_fd" == 'keep' ]]; then | ||
echo "Can't keep defaults for nonexistent log level: $log_level" >&2 | ||
exit 1 | ||
fi | ||
__go_log_level_index="${#_GO_LOG_LEVELS[@]}" | ||
__GO_LOG_LEVELS+=("$log_level") | ||
__GO_LOG_LEVELS_FORMAT_CODES+=("$format_code") | ||
__GO_LOG_LEVELS_FILE_DESCRIPTORS+=("$level_fd") | ||
return | ||
fi | ||
|
||
__GO_LOG_LEVELS[$__go_log_level_index]="$log_level" | ||
|
||
if [[ "$format_code" != 'keep' ]]; then | ||
__GO_LOG_LEVELS_FORMAT_CODES[$__go_log_level_index]="$format_code" | ||
fi | ||
if [[ "$level_fd" != 'keep' ]]; then | ||
__GO_LOG_LEVELS_FILE_DESCRIPTORS[$__go_log_level_index]="$level_fd" | ||
fi | ||
} | ||
|
||
# Runs the project's 'setup' script and logs the result. | ||
# | ||
# Helps with automating first-time setup upon running the script in a | ||
# freshly-cloned repository for the first time. | ||
# | ||
# You must have an executable script called 'setup' in your top-level script | ||
# directory before calling this function. If the script returns an error, the | ||
# process will exit with the status code returned by the script. | ||
# | ||
# For example, in a Node.js project, your ./go script may include the following: | ||
# | ||
# export PATH="node_modules/.bin:$PATH" | ||
# if [[ ! -d 'node_modules' ]]; then | ||
# @go.setup_project | ||
# else | ||
# @go "$@" | ||
# fi | ||
# | ||
# And your 'setup' script may include (assuming your own 'test' script as well): | ||
# | ||
# @go.critical_section_begin | ||
# @go.log_command npm install | ||
# @go.log_command @go test | ||
# @go.critical_section_end | ||
# | ||
# Arguments: | ||
# Any arguments to pass through to the 'setup' script | ||
@go.setup_project() { | ||
local setup_script="$_GO_SCRIPTS_DIR/setup" | ||
local setup_status | ||
|
||
@go.log START Project setup in "$_GO_ROOTDIR" | ||
|
||
if [[ ! -f "$setup_script" ]]; then | ||
@go.log FATAL 1 "Create $setup_script before invoking $FUNCNAME." | ||
elif [[ ! -x "$setup_script" ]]; then | ||
@go.log FATAL 1 "$setup_script is not executable." | ||
fi | ||
|
||
@go.log RUN "${setup_script#$_GO_ROOTDIR/}" "$@" | ||
_@go.run_command_script "$setup_script" "$@" | ||
setup_status="$?" | ||
|
||
if [[ "$setup_status" -ne '0' ]]; then | ||
@go.log FATAL "$setup_status" "Project setup failed" | ||
fi | ||
|
||
@go.log FINISH Project setup successful | ||
@go.log INFO "Run \`$0 help\` to see the available commands." | ||
|
||
if [[ "$_GO_CMD" == "$0" ]]; then | ||
@go.log INFO \ | ||
"Run \`$0 help env\` to see how to set up your shell environment" \ | ||
'for this project.' | ||
fi | ||
} | ||
|
||
_@go.log_init() { | ||
if [[ -z "$__GO_LOG_INIT" ]]; then | ||
_@go.log_format_level_labels | ||
readonly __GO_LOG_INIT='done' | ||
fi | ||
} | ||
|
||
_@go.log_format_level_labels() { | ||
local num_levels="${#_GO_LOG_LEVELS[@]}" | ||
local label_length | ||
local longest_label_length | ||
local padding='' | ||
local log_level | ||
local padding_len | ||
local level_var | ||
local level_fd | ||
local i | ||
|
||
for ((i=0; i != num_levels; ++i)); do | ||
label_length="${#_GO_LOG_LEVELS[$i]}" | ||
if [[ "$label_length" -gt "$longest_label_length" ]]; then | ||
longest_label_length="$label_length" | ||
fi | ||
done | ||
|
||
for ((i=0; i != longest_label_length; ++i)); do | ||
padding+=' ' | ||
done | ||
|
||
for ((i=0; i != num_levels; ++i)); do | ||
log_level="${_GO_LOG_LEVELS[$i]}" | ||
padding_len="$((${#padding} - ${#log_level}))" | ||
level_fd="${__GO_LOG_LEVELS_FILE_DESCRIPTORS[$i]}" | ||
|
||
if [[ -n "$_GO_LOG_FORMATTING" || -t "$level_fd" ]]; then | ||
log_level="${__GO_LOG_LEVELS_FORMAT_CODES[$i]}$log_level\e[0m" | ||
fi | ||
__GO_LOG_LEVELS_FORMATTED[$i]="${log_level}${padding:0:$padding_len}" | ||
done | ||
} | ||
|
||
_@go.log_level_index() { | ||
local i | ||
for ((i=0; i != "${#_GO_LOG_LEVELS[@]}"; ++i)); do | ||
if [[ "${_GO_LOG_LEVELS[$i]}" == "$1" ]]; then | ||
__go_log_level_index="$i" | ||
return | ||
fi | ||
done | ||
return 1 | ||
} | ||
|
||
_@go.log_load() { | ||
local num_levels="${#_GO_LOG_LEVELS[@]}" | ||
|
||
if [[ "${#__GO_LOG_LEVELS_FORMAT_CODES[@]}" != "$num_levels" ]]; then | ||
echo "Should have $num_levels log level format codes, " \ | ||
"only have ${#__GO_LOG_LEVELS_FORMAT_CODES[@]}" >&2 | ||
exit 1 | ||
elif [[ "${#__GO_LOG_LEVELS_FILE_DESCRIPTORS[@]}" != "$num_levels" ]]; then | ||
echo "Should have $num_levels log level file descriptors, " \ | ||
"only have ${#__GO_LOG_LEVELS_FILE_DESCRIPTORS[@]}" >&2 | ||
exit 1 | ||
fi | ||
} | ||
|
||
_@go.log_load |