diff --git a/cli.bash b/cli.bash new file mode 100644 index 0000000..9792ce9 --- /dev/null +++ b/cli.bash @@ -0,0 +1,139 @@ +#!/bin/usr/env bash + +import log + +declare -Ag cli__options=() +declare -Ag cli__descriptions=() +cli__program_name="$0" +cli__order=() + +cli::reset() { + cli__order=() + declare -Ag cli__options=() + declare -Ag cli__descriptions=() +} + +cli::set_program_name() { + local program_name="$1" + + cli__program_name="${program_name}" +} + +cli::_option_encode() { + local option="$1" + + log::debug "Encoding ${option}" + + echo "${option}" +} + +cli::_option_decode() { + local option="$1" + + log::debug "Decoding ${option}" + + echo "${option}" +} + +# if letter require argument, then subfix letter with ':' +cli::add_option() { + local letter="$1" + local callback="$2" + local description="$3" + + cli__order+=("$letter") + cli__options["${letter:0:1}"]="$callback" + cli__descriptions[$letter]="$description" +} + +cli::print_help() { + local usage="Usage: ${cli__program_name}" + local description="" + local description_formatted="" + log::debug "${!cli__descriptions[@]}" + log::debug "values: ${cli__descriptions[*]}" + for letter in "${cli__order[@]}";do + description_formatted="${cli__descriptions["$letter"]//\\n/\\n }" + if [ "${letter:1:1}" = ":" ];then + usage+=" [-${letter:0:1} ...]" + description+=" -${letter:0:1} <...> ${description_formatted}\n" + else + usage+=" [-${letter:0:1}]" + description+=" -${letter:0:1} ${description_formatted}\n" + fi + if [ "$(echo -e "$usage" | tail -n1 | wc -m )" -gt 70 ];then + usage+="\n " + fi + done + echo -e "${usage}\n\nOptions:\n${description}" +} + +cli::handle() { + local all_options="" + for letter in "${cli__order[@]}";do + all_options+="${letter}" + done + + while getopts ":${all_options}" opt; do + if [ "$opt" = ":" ];then + log::fatal "Option -${OPTARG} require an argument" + elif [ "$opt" = "?" ];then + log::fatal "Invalid option -${OPTARG}" + fi + ${cli__options[$opt]} "${OPTARG}" + done +} + +cli_test__a=0 +cli_test__b=0 +cli_test__c=0 + +cli_test::a_cb() { + cli_test__a=1 +} + +cli_test::b_cb() { + cli_test__b=$1 +} + +cli_test::c_cb() { + cli_test__c=1 +} + +cli_test::test_handle() { + cli::reset + cli::set_program_name "example" + cli::add_option "a" cli_test::a_cb "Example option a" + cli::add_option "b:" cli_test::b_cb "Example option b" + cli::add_option "c" cli_test::c_cb "Example option c" + + cli::handle "-a" "-b" "sth" + unit::assert_eq "${cli_test__a}" "1" + unit::assert_eq "${cli_test__b}" "sth" + unit::assert_eq "${cli_test__c}" "0" +} + +cli_test::test_help_gen() { + cli::reset + cli::set_program_name "example" + cli::add_option "a" cli_test::a_cb "Example option a" + cli::add_option "b:" cli_test::b_cb "Example option b" + cli::add_option "c" cli_test::c_cb "Example option c" + + local expected_output + expected_output="Usage: example [-a] [-b ...] [-c] + +Options: + -a Example option a + -b <...> Example option b + -c Example option c" + local output + output=$(cli::print_help) + + unit::assert_eq "${output}" "${expected_output}" +} + +cli_test::all() { + unit::test cli_test::test_help_gen + unit::test cli_test::test_handle +} diff --git a/exec.bash b/exec.bash index 82c5e3b..c117f28 100644 --- a/exec.bash +++ b/exec.bash @@ -20,16 +20,44 @@ exec::silent() { fi } +exec::retried_exec() { + local retries=$1 + local sleep=$2 + shift;shift + local try + local log + for ((try=0; try&1)" &>/dev/null;then + return 0 + fi + if [ "$DEBUG" = "1" ];then + echo "$log" >&2 + fi + sleep "$sleep" + } + log::error "Command $* failed $retries times" + log::error "$log" + return 1 +} + exec__sudo_keeping=0 exec::sudo_keep_alive() { + exec::assert_cmd "sudo" if [ "$exec__sudo_keeping" != "1" ];then exec__sudo_keeping=1 - sudo -v + if ! sudo -v;then + log::fatal "sudo authentication failed" + fi while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & fi } +exec::sudo() { + exec::sudo_keep_alive + sudo "$@" +} + exec::is_cmd_available() { local cmd="$1" if command -v "$cmd" &>/dev/null;then @@ -49,7 +77,7 @@ exec::is_fn() { exec::assert_cmd() { local cmd="$1" if ! exec::is_cmd_available "$cmd";then - log::panic "Command \`$cmd\` is not available, but is required by this application" + log::fatal "Command \`$cmd\` is not available, but is required by this application" fi } diff --git a/fetch.bash b/fetch.bash index abeb65c..9687a4b 100644 --- a/fetch.bash +++ b/fetch.bash @@ -23,6 +23,15 @@ fetch::download_stream() { curl -sfL "$url" } +fetch::file_exists() { + local url=$1 + if curl --output /dev/null --silent --head --fail -L "$url";then + return 0 + else + return 1 + fi +} + fetch::verify() { local sha=$1 local file=$2 diff --git a/github.bash b/github.bash index b6a77a9..a9f43cd 100644 --- a/github.bash +++ b/github.bash @@ -5,7 +5,7 @@ github::latest_release() { local _repo=$1 local -n output_version=$2 local _url - _url=$(curl -w "%{url_effective}" -I -L -s -S "https://github.com/$_repo/releases/latest" -o /dev/null) + _url=$(curl -f -w "%{url_effective}" -I -L -s -S "https://github.com/$_repo/releases/latest" -o /dev/null) if [ "$_url" != "https://github.com/$_repo/releases" ];then # shellcheck disable=SC2034 output_version=$(echo -n "$_url"| sed -e 's|.*/||') @@ -33,7 +33,7 @@ github::latest_commit() { local -n output_sha=$3 exec::assert_cmd jq local _json - _json=$(curl -s -L "https://api.github.com/repos/$_repo/commits/$_branch") + _json=$(curl -f -s -L "https://api.github.com/repos/$_repo/commits/$_branch") local _sha="" if _sha=$(echo -n "$_json" | jq '.sha' -re);then # shellcheck disable=SC2034 diff --git a/installer.bash b/installer.bash index 5568ce5..6f72633 100644 --- a/installer.bash +++ b/installer.bash @@ -85,7 +85,7 @@ installer::copy_file() { if [ -f "$dst" ];then local backup="" tmp::create_persistent_file backup "backup" "$installer__files_dir" - cp "$dst" "$backup" + cp -p "$dst" "$backup" local relative_backup="${backup##$installer__dir/}" installer::_add_sha_item "$installer__dir" "$relative_backup" installer::_write_line "log 'Restoring $dst'" diff --git a/log.bash b/log.bash index dbed36f..78cecf9 100644 --- a/log.bash +++ b/log.bash @@ -1,6 +1,15 @@ #!/usr/bib/env bash log__debug=0 +log__colors=-1 + +log::detect_colors() { + if [ -t 1 ];then + log__colors=1 + else + log__colors=0 + fi +} log::enable_debug() { log__debug=1 @@ -10,10 +19,27 @@ log::disable_debug() { log__debug=0 } +log::disable_colors() { + log__colors=0 + log::debug "Colors forcefuly disabled" +} + +log::enable_colors() { + log__colors=1 + log::debug "Colors forcefuly enabled" +} + log::debug() { local pid=$BASHPID if [ "$log__debug" = "1" ] || [ "$DEBUG" = "1" ];then - echo -e "\e[90m$(printf "%-6s %-30s" "$pid" "${FUNCNAME[1]}"): $*\e[0m" >&2 + if [ "$log__colors" = "1" ];then + echo -e "\e[90m$(printf "%-6s %-30s" "$pid" "${FUNCNAME[1]}"): $*\e[0m" >&2 + elif [ "$log__colors" = "-1" ];then + log::detect_colors + log::debug "$@" + else + echo -e "[DEBUG ] $(printf "%-6s %-30s" "$pid" "${FUNCNAME[1]}"): $*" >&2 + fi fi } @@ -22,18 +48,51 @@ log::panic() { stack::print 1 1 } +log::fatal() { + log::error "$*" + exit 1 +} + log::success() { - echo -e "\e[32m$*\e[0m" >&2 + if [ "$log__colors" = "1" ];then + echo -e "\e[32m$*\e[0m" >&2 + elif [ "$log__colors" = "-1" ];then + log::detect_colors + log::success "$@" + else + echo -e "[SUCCESS] $*" >&2 + fi } log::info() { - echo -e "\e[36m$*\e[0m" >&2 + if [ "$log__colors" = "1" ];then + echo -e "\e[36m$*\e[0m" >&2 + elif [ "$log__colors" = "-1" ];then + log::detect_colors + log::info "$@" + else + echo -e "[INFO ] $*" >&2 + fi } log::error() { - echo -e "\e[31m$*\e[0m" >&2 + if [ "$log__colors" = "1" ];then + echo -e "\e[31m$*\e[0m" >&2 + elif [ "$log__colors" = "-1" ];then + log::detect_colors + log::error "$@" + else + echo -e "[ERROR ] $*" >&2 + fi } log::warn() { - echo -e "\e[33m$*\e[0m" >&2 + if [ "$log__colors" = "1" ];then + echo -e "\e[33m$*\e[0m" >&2 + elif [ "$log__colors" = "-1" ];then + log::detect_colors + log::warn "$@" + else + echo -e "[WARN ] $*" >&2 + fi } diff --git a/stack.bash b/stack.bash index f22806a..5fc15ef 100644 --- a/stack.bash +++ b/stack.bash @@ -5,6 +5,10 @@ stack::print() { set +o xtrace local code="${1:-1}" local up=${2:-0} + # Ignore errors detected in bashdb + if [[ ${FUNCNAME[$up+1]} == _Dbg_* ]];then + return + fi echo -e "\n\e[91mRuntime failure at ${BASH_SOURCE[$up+1]}:${BASH_LINENO[$up]} exit code $err\n" stack::point_line "${BASH_SOURCE[$up+1]}" "${BASH_LINENO[$up]}" if [ ${#FUNCNAME[@]} -gt $((up+2)) ];then @@ -41,6 +45,7 @@ stack::point_line() { trap stack::print ERR set -o errtrace +set -E import traps traps::add_err_trap "stack::print 1 1" diff --git a/test.sh b/test.sh index 4f9ea42..172248e 100755 --- a/test.sh +++ b/test.sh @@ -10,6 +10,7 @@ import dirdb import tmp import installer import daemon +import cli test_fails() { unit::assertEq 1 2 "this is error" @@ -23,3 +24,4 @@ dirdb_test::all tmp_test::all installer_test::all daemon_test::all +cli_test::all diff --git a/traps.bash b/traps.bash index 5009870..30ad2bb 100644 --- a/traps.bash +++ b/traps.bash @@ -41,17 +41,18 @@ traps::_handle_int() { } traps::_handle_err() { + local err=$? log::debug "Handling ERR traps" if [ "$traps__err_ignore" = "1" ];then return 0 fi for cb in "${traps__err_trap[@]}";do - $cb + $cb "$err" done } traps::_init() { - if [ "$traps__set_up" != "$BASHPID" ] && [ -z "$BASH_LIB_UNDER_TEST" ];then + if [ "$traps__set_up" = "0" ] && [ -z "$BASH_LIB_UNDER_TEST" ];then log::debug "Setting up traps for $BASHPID" traps__set_up=$BASHPID traps__exit_trap=() diff --git a/xdg.bash b/xdg.bash index d9a3e24..5acbd79 100644 --- a/xdg.bash +++ b/xdg.bash @@ -27,9 +27,12 @@ xdg::_find_file_in_paths() { local filename="$1" local paths="$2" local directories=() - mapfile -d' ' directories <<< "$paths" + mapfile -d':' -t directories <<< "$paths" for dir in "${directories[@]}";do - if [ -d "$dir/$filename" ];then + dir=$(echo -n "$dir"|tr -d "\n") + log::debug "looking for $dir/$filename..." + if [ -e "$dir/$filename" ];then + log::debug "found $dir/$filename" echo -n "$dir/$filename" return fi @@ -50,7 +53,3 @@ xdg::find_config_file() { xdg::_find_file_in_paths "$filename" "$config_dirs" } -xdg_test::test_find_data_file() { - XDG_DATA_DIRS= -} -