From 0d959f6cf94d2d6a9703f10dd482a1781895627c Mon Sep 17 00:00:00 2001 From: tearran Date: Wed, 12 Jul 2023 08:36:48 -0700 Subject: [PATCH 01/48] Setup for root copy --- .gitignore | 2 - bin/cli_options.sh | 90 + bin/cpu_test.sh | 214 ++ bin/jampi-config | 212 ++ functions/bash-utility-master/.editorconfig | 22 - functions/bash-utility-master/.gitignore | 3 - functions/bash-utility-master/.remarkrc | 8 - .../src => lib/bash-utility}/array.sh | 0 .../src => lib/bash-utility}/check.sh | 0 .../src => lib/bash-utility}/collection.sh | 0 .../src => lib/bash-utility}/date.sh | 0 .../src => lib/bash-utility}/debug.sh | 0 .../src => lib/bash-utility}/file.sh | 0 .../src => lib/bash-utility}/format.sh | 0 .../src => lib/bash-utility}/interaction.sh | 0 .../src => lib/bash-utility}/json.sh | 0 .../src => lib/bash-utility}/misc.sh | 0 .../src => lib/bash-utility}/os.sh | 0 .../src => lib/bash-utility}/string.sh | 0 .../src => lib/bash-utility}/terminal.sh | 0 .../src => lib/bash-utility}/validation.sh | 0 .../src => lib/bash-utility}/variable.sh | 0 lib/configng/cpu.sh | 267 ++ .../bash-utility}/CODE_OF_CONDUCT.md | 0 .../bash-utility}/CONTRIBUTING.md | 0 .../bash-utility}/LICENSE | 0 .../bash-utility}/README.md | 0 .../bash-utility}/bash_utility.sh | 0 .../bash-utility}/bin/bashdoc.awk | 0 .../bash-utility}/bin/generate_readme.sh | 0 .../bash-utility}/image/bash-utility.png | Bin .../bash-utility}/image/logo.png | Bin src/bash-utility/src/array.sh | 284 ++ src/bash-utility/src/check.sh | 34 + src/bash-utility/src/collection.sh | 287 ++ src/bash-utility/src/date.sh | 744 ++++ src/bash-utility/src/debug.sh | 49 + src/bash-utility/src/file.sh | 222 ++ src/bash-utility/src/format.sh | 183 + src/bash-utility/src/interaction.sh | 96 + src/bash-utility/src/json.sh | 25 + src/bash-utility/src/misc.sh | 121 + src/bash-utility/src/os.sh | 135 + src/bash-utility/src/string.sh | 198 ++ src/bash-utility/src/terminal.sh | 51 + src/bash-utility/src/validation.sh | 244 ++ src/bash-utility/src/variable.sh | 144 + src/configng/README.md | 62 + .../bash-utility-master/CODE_OF_CONDUCT.md | 76 + .../bash-utility-master/CONTRIBUTING.md | 129 + .../functions/bash-utility-master/LICENSE | 21 + .../functions/bash-utility-master/README.md | 3026 +++++++++++++++++ .../bash-utility-master/bash_utility.sh | 20 + .../bash-utility-master/bin/bashdoc.awk | 275 ++ .../bin/generate_readme.sh | 400 +++ .../image/bash-utility.png | Bin 0 -> 32396 bytes .../bash-utility-master/image/logo.png | Bin 0 -> 23716 bytes .../bash-utility-master/src/array.sh | 284 ++ .../bash-utility-master/src/check.sh | 34 + .../bash-utility-master/src/collection.sh | 287 ++ .../functions/bash-utility-master/src/date.sh | 744 ++++ .../bash-utility-master/src/debug.sh | 49 + .../functions/bash-utility-master/src/file.sh | 222 ++ .../bash-utility-master/src/format.sh | 183 + .../bash-utility-master/src/interaction.sh | 96 + .../functions/bash-utility-master/src/json.sh | 25 + .../functions/bash-utility-master/src/misc.sh | 121 + .../functions/bash-utility-master/src/os.sh | 168 + .../bash-utility-master/src/string.sh | 198 ++ .../bash-utility-master/src/terminal.sh | 51 + .../bash-utility-master/src/validation.sh | 244 ++ .../bash-utility-master/src/variable.sh | 144 + {functions => src/configng/functions}/cpu.sh | 0 {test => src/configng/test}/cpu_test.sh | 0 74 files changed, 10459 insertions(+), 35 deletions(-) delete mode 100644 .gitignore create mode 100644 bin/cli_options.sh create mode 100755 bin/cpu_test.sh create mode 100755 bin/jampi-config delete mode 100644 functions/bash-utility-master/.editorconfig delete mode 100644 functions/bash-utility-master/.gitignore delete mode 100644 functions/bash-utility-master/.remarkrc rename {functions/bash-utility-master/src => lib/bash-utility}/array.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/check.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/collection.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/date.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/debug.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/file.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/format.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/interaction.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/json.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/misc.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/os.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/string.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/terminal.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/validation.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/variable.sh (100%) mode change 100755 => 100644 create mode 100644 lib/configng/cpu.sh rename {functions/bash-utility-master => src/bash-utility}/CODE_OF_CONDUCT.md (100%) rename {functions/bash-utility-master => src/bash-utility}/CONTRIBUTING.md (100%) rename {functions/bash-utility-master => src/bash-utility}/LICENSE (100%) rename {functions/bash-utility-master => src/bash-utility}/README.md (100%) rename {functions/bash-utility-master => src/bash-utility}/bash_utility.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master => src/bash-utility}/bin/bashdoc.awk (100%) mode change 100755 => 100644 rename {functions/bash-utility-master => src/bash-utility}/bin/generate_readme.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master => src/bash-utility}/image/bash-utility.png (100%) rename {functions/bash-utility-master => src/bash-utility}/image/logo.png (100%) create mode 100644 src/bash-utility/src/array.sh create mode 100644 src/bash-utility/src/check.sh create mode 100644 src/bash-utility/src/collection.sh create mode 100644 src/bash-utility/src/date.sh create mode 100644 src/bash-utility/src/debug.sh create mode 100644 src/bash-utility/src/file.sh create mode 100644 src/bash-utility/src/format.sh create mode 100644 src/bash-utility/src/interaction.sh create mode 100644 src/bash-utility/src/json.sh create mode 100644 src/bash-utility/src/misc.sh create mode 100644 src/bash-utility/src/os.sh create mode 100644 src/bash-utility/src/string.sh create mode 100644 src/bash-utility/src/terminal.sh create mode 100644 src/bash-utility/src/validation.sh create mode 100644 src/bash-utility/src/variable.sh create mode 100644 src/configng/README.md create mode 100644 src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md create mode 100644 src/configng/functions/bash-utility-master/CONTRIBUTING.md create mode 100644 src/configng/functions/bash-utility-master/LICENSE create mode 100644 src/configng/functions/bash-utility-master/README.md create mode 100644 src/configng/functions/bash-utility-master/bash_utility.sh create mode 100644 src/configng/functions/bash-utility-master/bin/bashdoc.awk create mode 100644 src/configng/functions/bash-utility-master/bin/generate_readme.sh create mode 100644 src/configng/functions/bash-utility-master/image/bash-utility.png create mode 100644 src/configng/functions/bash-utility-master/image/logo.png create mode 100644 src/configng/functions/bash-utility-master/src/array.sh create mode 100644 src/configng/functions/bash-utility-master/src/check.sh create mode 100644 src/configng/functions/bash-utility-master/src/collection.sh create mode 100644 src/configng/functions/bash-utility-master/src/date.sh create mode 100644 src/configng/functions/bash-utility-master/src/debug.sh create mode 100644 src/configng/functions/bash-utility-master/src/file.sh create mode 100644 src/configng/functions/bash-utility-master/src/format.sh create mode 100644 src/configng/functions/bash-utility-master/src/interaction.sh create mode 100644 src/configng/functions/bash-utility-master/src/json.sh create mode 100644 src/configng/functions/bash-utility-master/src/misc.sh create mode 100644 src/configng/functions/bash-utility-master/src/os.sh create mode 100644 src/configng/functions/bash-utility-master/src/string.sh create mode 100644 src/configng/functions/bash-utility-master/src/terminal.sh create mode 100644 src/configng/functions/bash-utility-master/src/validation.sh create mode 100644 src/configng/functions/bash-utility-master/src/variable.sh rename {functions => src/configng/functions}/cpu.sh (100%) rename {test => src/configng/test}/cpu_test.sh (100%) mode change 100755 => 100644 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 58c32c89c..000000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/.project -/.settings/ diff --git a/bin/cli_options.sh b/bin/cli_options.sh new file mode 100644 index 000000000..0062725b6 --- /dev/null +++ b/bin/cli_options.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# + +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") + +libpath="$directory/../lib" +selfpath="$libpath/configng/cpu.sh" + +if [[ -d "$directory/../lib" ]]; then + libpath="$directory"/../lib +elif [[ -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then + libpath="/usr/lib" +else + echo "Libraries not found" + exit 0 +fi + +# Source the files relative to the script location +source "$libpath/bash-utility/string.sh" +source "$libpath/bash-utility/collection.sh" +source "$libpath/bash-utility/array.sh" +source "$libpath/bash-utility/check.sh" +source "$libpath/configng/cpu.sh" + +readarray -t functionarray < <(grep -oP '^\w+::\w+' "$selfpath") +readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/.*:://') +readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/::.*//') +readarray -t descriptionarray < <(grep -oP '^# @description.*' "$selfpath" | sed 's/^# @description //') + +# test array +#printf '%s\n' "${functionarray[@]}" +#exit 0 + +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -dev ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" + done + + } + +# TEST 3 +# check for -h -dev @ $1 +# if -dev check @ $1 remove and shift $1 to check for x +check_opts() { + if [ "$1" == "dev" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-dev") + function_name="$1" # Assigns the next argument as the function name + shift # Shifts the arguments again to exclude the function name + + found=false + + for ((i=0; i<${#functionarray[@]}; i++)); do + if [ "$function_name" == "${functionarray[i]}" ]; then + found=true + ${functionarray[i]} "$@" + break + fi + done + + if [ "$found" == false ]; then + echo "Invalid function name" + fi + + elif [[ "$1" == "dev" && "$2" == "cpu::set_freq" ]]; then + # Disabled till understood. + echo "cpu::set_freq policy min_freq max_freq performance" + echo "Disabled durring current testing" + + else + see_help + fi +} + +#check_opts_test1 "$@" +check_opts "$@" + diff --git a/bin/cpu_test.sh b/bin/cpu_test.sh new file mode 100755 index 000000000..9a50d4954 --- /dev/null +++ b/bin/cpu_test.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# CPU related tests. +# + + +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") + +#libpath="$directory/../lib" +#selfpath="$libpath/configng/cpu.sh" + +if [[ -d "$directory/../lib" ]]; then + libpath="$directory"/../lib +elif [[ -d "/usr/lib/bash-utility" && -d "/usr/lib/configng" ]]; then + libpath="/usr/lib" +elif [[ -d "/../functions/bash-utility-master/src" ]] ; then + libpath="$directory"/../functions/bash-utility-master/src +else + echo "Libraries not found" + exit 0 +fi + +# Source the files relative to the script location +source "$libpath/bash-utility/string.sh" +source "$libpath/bash-utility/collection.sh" +source "$libpath/bash-utility/array.sh" +source "$libpath/bash-utility/check.sh" +source "$libpath/configng/cpu.sh" + +# Rest of the script... +# @description Print value from collection. +# +# @example +# collection::each "print_func" +# #Output +# Value in collection +print_func(){ + printf "%s\n" "$1" + return 0 + } + +# @description Check function exit code and exit script if != 0. +# +# @example +# check_return +# #Output +# Nothing +check_return(){ + if [ "$?" -ne 0 ]; then + exit 1 + fi + } + +see_cpu(){ +# Get policy +declare -i policy=$(cpu::get_policy) +printf 'Policy = %d\n' "$policy" +declare -i min_freq=$(cpu::get_min_freq $policy) +check_return +printf 'Minimum frequency = %d\n' "$min_freq" +declare -i max_freq=$(cpu::get_max_freq $policy) +check_return +printf 'Maximum frequency = %d\n' "$max_freq" +governor=$(cpu::get_governor $policy) +check_return +printf 'Governor = %s\n' "$governor" + +# Return frequencies as array +declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) +check_return +printf "\nAll frequencies\n" + +# Print all values in collection +printf "%s\n" "${freqs[@]}" | collection::each "print_func" +declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) +check_return +printf "\nAll governors\n" + + +# Print all values in collection +printf "%s\n" "${governors[@]}" | collection::each "print_func" + + +# Are we running as sudo? +[[ "$EUID" != 0 ]] && printf "Must call cpu::set_freq as sudo\n" && exit 1 + +# Before +printf "\nBefore\n" +cat /etc/default/cpufrequtils +cpu::set_freq $policy "$min_freq" "$max_freq" performance + +# After +printf "\nAfter\n" +cat /etc/default/cpufrequtils + +} + +readarray -t functionarray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh") +readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh" | sed 's/.*:://') +readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$libpat/configng/cpu.sh" | sed 's/::.*//') +readarray -t descriptionarray < <(grep -oP '^# @description.*' "$libpath/configng/cpu.sh" | sed 's/^# @description //') + +#printf '%s\n' "${functionarray[@]}" +#exit 0 +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -dev | ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " -dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" + done + echo -e " -cpu Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s\n' "${funnamearray[i]}" "${descriptionarray[i]}" + done + + } +# check for -dev -h options +check_opts_test1() +{ + if [[ "$1" == -dev ]] ; then + default="bash" + local found=false + for i in "${!functionarray[@]}"; do + if [ "$2" == "${functionarray[i]}" ]; then + "${functionarray[i]}" + found=true + break + fi + done + if ! $found; then + see_help + exit 0 + fi + elif [[ "$1" == "-cpu" ]] ; then + echo -e " " + echo -e " TODO:" + for i in "${!functionarray[@]}"; do + + printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" + done + + elif [[ "$1" == -h ]] ; then + see_help + else + see_cpu + fi +} + +# check for -h -dev +# if -dev check for number +check_opts_test2(){ + +if [ "$1" == "-dev" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-dev") + function_name="$1" # Assigns the next argument as the function name + shift # Shifts the arguments again to exclude the function name + + case "$function_name" in + 0) echo "Calling function 0 with arguments: $@" ;; + 1) echo "Calling function 1 with arguments: $@" ;; + 2) echo "Calling function 2 with arguments: $@" ;; + 3) echo "Calling function 3 with arguments: $@" ;; + 4) echo "Calling function 4 with arguments: $@" ;; + *) echo "Invalid function name" ;; + esac +else + echo "No -dev flag found" +fi + +} +# check for -h -dev @ $1 +# if -dev check @ $1 remove and shift $1 to check for x +check_opts() { + if [ "$1" == "-dev" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-dev") + function_name="$1" # Assigns the next argument as the function name + shift # Shifts the arguments again to exclude the function name + + found=false + + for ((i=0; i<${#functionarray[@]}; i++)); do + if [ "$function_name" == "${functionarray[i]}" ]; then + found=true + ${functionarray[i]} "$@" + break + fi + done + + if [ "$found" == false ]; then + echo "Invalid function name" + fi + + elif [ "$1" == "-h" ]; then + see_help + else + see_cpu + fi +} + +#check_opts_test1 "$@" +check_opts "$@" + +#cpu::set_freq $policy "$min_freq" "$max_freq" performance diff --git a/bin/jampi-config b/bin/jampi-config new file mode 100755 index 000000000..4dd17d7c6 --- /dev/null +++ b/bin/jampi-config @@ -0,0 +1,212 @@ +#!/bin/bash + +# +# Copyright (c) 2023 Joseph C Turner +# All rights reserved. +# +# This script. +# demonstrates the compatibility of multiple interfaces for displaying menus or messages. +# It uses an array to set the options for all three menus (bash, whiptail, and dialog). +# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. +# If neither of these programs is available, it falls back to using bash. +# while both are installed falls back to whiptail to display the menu. +# The user can override the default program by passing an argument when running the script: +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + + + +#DIRECTORY variable to the absolute path of the script's directory +#directory=$(cd "$(dirname "$0")" && pwd) +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") +selfpath="$directory"/"$filename" +#libpath="${directory}"/"its-lib" #Include these scripts Library +libpath="$selfpath" + + +clear +# Check for the availability of whiptail and dialog command-line programs +# Set the default program to use for displaying messages +( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" +( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" +( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" + +# if both whiptail and dialog change to prefered +( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" + + +[[ "$1" == -b ]] && default="bash" +[[ "$1" == -w ]] && default="whiptail" +[[ "$1" == -n ]] && default="dialog" + +[[ "$1" == -m ]] && { +export NEWT_COLORS=" +root=blue,black +border=green,black +title=green,black +roottext=red,black +window=red,black +textbox=white,black +button=black,green +compactbutton=white,black +listbox=white,black +actlistbox=black,white +actsellistbox=black,green +checkbox=green,black +actcheckbox=black,green +" +} + +# Check the cpu architecture. for later handeling if nessery +architecture=$(dpkg --print-architecture) +[[ "$architecture" == "armf" ]] && true ; + +#setup menu arrays +readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) +readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) +readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) + +## System@Settings:Advanced Settings (armbian-config) +cmd_advance() +{ + sudo armbian-config +} + +## System@CPU info:Example from Bash Utility (cpu_test.sh) +cmd_cpu_info() +{ + [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; + [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +## System@CPU info:An example function (lscpu) +cmd_cpu_ls() +{ + + shell_command="$(lscpu)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Bootup Time:An example function (systemd-analyze time) +cmd_boot_time() +{ + + shell_command="$(systemd-analyze time)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Login Info :An example function (Login Info) +cmd_lslogins() +{ + + shell_command="$(lslogins)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +# Function to display a message using whiptail, dialog or printf depending on what is available on the system +see_message() +{ + # Use if neither whiptail nor dialog are available on the system + if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then + # Use printf to display the message + printf '%s ' "${shell_command[@]}" + + # Use as default if whiptail is available + elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then + # Use whiptail to display the message + whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext + + # Use if dialog is available on the system but not whiptail + elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then + # Use dialog to display the message + dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear + fi +} + +see_menu() +{ + + if [[ "$default" == "bash" ]]; then + PS3="Enter a number: " + select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done + + elif [[ "$default" == "whiptail" ]]; then + OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + + elif [[ "$default" == "dialog" ]]; then + OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + fi +} + +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " -b GNU bash " + echo -e " -n NCURSES dialog " + echo -e " -w NEWT whiptail - default colors " + echo -e " -m dark mode whiptail " + echo -e " -dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" + done + + } + +main() +{ + if [[ "$1" == --dev ]] ; then + default="bash" + local found=false + for i in "${!functionarray[@]}"; do + if [ "$2" == "${functionarray[i]}" ]; then + "${functionarray[i]}" + found=true + break + fi + done + if ! $found; then + see_help + exit 0 + fi + elif [[ "$1" == -h ]] ; then + see_help + else + see_menu + fi +} + +main "$@" diff --git a/functions/bash-utility-master/.editorconfig b/functions/bash-utility-master/.editorconfig deleted file mode 100644 index f9b075876..000000000 --- a/functions/bash-utility-master/.editorconfig +++ /dev/null @@ -1,22 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -# Unix-style newlines with a newline ending every file -[*] -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -# for shfmt -[*.sh] -indent_style = space -indent_size = 4 -shell_variant = bash -switch_case_indent = true -space_redirects = true - -[*.md] -trim_trailing_whitespace = false \ No newline at end of file diff --git a/functions/bash-utility-master/.gitignore b/functions/bash-utility-master/.gitignore deleted file mode 100644 index 7c7bf7091..000000000 --- a/functions/bash-utility-master/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/tmp -gh-pages -hugo-docs diff --git a/functions/bash-utility-master/.remarkrc b/functions/bash-utility-master/.remarkrc deleted file mode 100644 index a3ff1e10d..000000000 --- a/functions/bash-utility-master/.remarkrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "plugins": [ - "remark-preset-lint-markdown-style-guide", - ["remark-lint-list-item-spacing", false], - ["remark-lint-maximum-line-length", false], - ["remark-lint-no-duplicate-headings", false] - ] -} diff --git a/functions/bash-utility-master/src/array.sh b/lib/bash-utility/array.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/array.sh rename to lib/bash-utility/array.sh diff --git a/functions/bash-utility-master/src/check.sh b/lib/bash-utility/check.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/check.sh rename to lib/bash-utility/check.sh diff --git a/functions/bash-utility-master/src/collection.sh b/lib/bash-utility/collection.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/collection.sh rename to lib/bash-utility/collection.sh diff --git a/functions/bash-utility-master/src/date.sh b/lib/bash-utility/date.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/date.sh rename to lib/bash-utility/date.sh diff --git a/functions/bash-utility-master/src/debug.sh b/lib/bash-utility/debug.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/debug.sh rename to lib/bash-utility/debug.sh diff --git a/functions/bash-utility-master/src/file.sh b/lib/bash-utility/file.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/file.sh rename to lib/bash-utility/file.sh diff --git a/functions/bash-utility-master/src/format.sh b/lib/bash-utility/format.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/format.sh rename to lib/bash-utility/format.sh diff --git a/functions/bash-utility-master/src/interaction.sh b/lib/bash-utility/interaction.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/interaction.sh rename to lib/bash-utility/interaction.sh diff --git a/functions/bash-utility-master/src/json.sh b/lib/bash-utility/json.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/json.sh rename to lib/bash-utility/json.sh diff --git a/functions/bash-utility-master/src/misc.sh b/lib/bash-utility/misc.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/misc.sh rename to lib/bash-utility/misc.sh diff --git a/functions/bash-utility-master/src/os.sh b/lib/bash-utility/os.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/os.sh rename to lib/bash-utility/os.sh diff --git a/functions/bash-utility-master/src/string.sh b/lib/bash-utility/string.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/string.sh rename to lib/bash-utility/string.sh diff --git a/functions/bash-utility-master/src/terminal.sh b/lib/bash-utility/terminal.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/terminal.sh rename to lib/bash-utility/terminal.sh diff --git a/functions/bash-utility-master/src/validation.sh b/lib/bash-utility/validation.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/validation.sh rename to lib/bash-utility/validation.sh diff --git a/functions/bash-utility-master/src/variable.sh b/lib/bash-utility/variable.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/variable.sh rename to lib/bash-utility/variable.sh diff --git a/lib/configng/cpu.sh b/lib/configng/cpu.sh new file mode 100644 index 000000000..772f9f4b1 --- /dev/null +++ b/lib/configng/cpu.sh @@ -0,0 +1,267 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. +# + +# @description Return policy as int based on original armbian-config logic. +# +# @example +# cpu::get_policy +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +# +# @stdout Policy as integer. +cpu::get_policy(){ + declare -i policy=0 + [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 + [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 + [[ -d /sys/devices/system/cpu/cpufreq/policy0 && -d /sys/devices/system/cpu/cpufreq/policy2 ]] && policy=2 + printf '%d\n' "$policy" +} + +# @description Return CPU frequencies as string delimited by space. +# +# @example +# cpu::get_freqs 0 +# echo $? +# #Output +# 648000 816000 912000 960000 1008000 1056000 1104000 1152000 +# +# @arg $1 int policy. +# +# @exitcode 0 If successful. +# @exitcode 1 If file not found. +# @exitcode 2 Function missing arguments. +# +# @stdout Space delimited string of CPU frequencies. +cpu::get_freqs(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_frequencies" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU minimum frequency as string. +# +# @example +# cpu::get_min_freq 0 +# echo $? +# #Output +# 648000 +# +# @arg $1 int policy. +# +# @exitcode 0 If successful. +# @exitcode 1 If file not found. +# @exitcode 2 Function missing arguments. +# +# @stdout CPU minimum frequency as string. +cpu::get_min_freq(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_min_freq" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU maximum frequency as string. +# +# @example +# cpu::get_max_freq 0 +# echo $? +# #Output +# 1152000 +# +# @arg $1 int policy. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout CPU maximum frequency as string. +cpu::get_max_freq(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_max_freq" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU governor as string. +# +# @example +# cpu::get_governor 0 +# echo $? +# #Output +# performance +# +# @arg $1 int policy. +cpu::get_governor(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_governor" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU governors as string delimited by space. +# +# @example +# cpu::get_governors 0 +# echo $? +# #Output +# performance +# +# @arg $1 int policy. +cpu::get_governors(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_governors" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Set min, max and CPU governor. +# +# @example +# cpu::set_freq 0 648000 1152000 performance +# echo $? +# #Output +# performance +# +# @arg $1 int Policy. +# @arg $2 int Minimum frequency. +# @arg $3 int Maximum frequency. +# @arg $4 string Governor. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode 3 Invalid minimum frequency. +# @exitcode 4 Invalid maximum frequency. +# @exitcode 5 Minimum frequency must be <= maximum frequency. +# @exitcode 6 Invalid governor. +cpu::set_freq(){ + # Check number of arguments + [[ $# -lt 4 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/etc/default/cpufrequtils" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + declare -i policy=$1 + declare -i min_freq=$2 + declare -i max_freq=$3 + local governor=$4 + # Return frequencies as array + declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) + # Validate minimum frequency + array::contains "$min_freq" ${freqs[@]} + [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 3 + # Validate maximum frequency + array::contains "$max_freq" ${freqs[@]} + [[ $? != 0 ]] && printf "%s: Invalid maximum frequency\n" "${FUNCNAME[0]}" && return 4 + # Validate minimum frequency is <= maximum frequency + [ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 + # Return governors + declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) + # Validate maximum governor + array::contains "$governor" ${governor[@]} + [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 6 + # Update file + sed -i "s/MIN_SPEED=.*/MIN_SPEED=$min_freq/" "$file" + sed -i "s/MAX_SPEED=.*/MAX_SPEED=$max_freq/" "$file" + sed -i "s/GOVERNOR=.*/GOVERNOR=$governor/" "$file" + # Return value + return 0 +} + + +# @description SetUp Virtula spi MTD FLash, Remove spi MTD FLash. +# +# @example +# storage::set_spi_vflash s +# echo $? +# #Output +# +# +# @arg $1 int UnSet. +# @arg $1 int SetUp. +# +# @exitcode 0 If successful. +storage::set_spi_vflash(){ + # TODO handeling + [[ "$1" == "setup" ]] && create_virt_spi + [[ "$1" == "remove" ]] && remove_virt_spi + +} + +create_virt_spi() +{ + # Load the nandsim and mtdblock modules to create a virtual MTD device + + sudo modprobe mtdblock + #sudo modprobe nandsim + # Find the newly created MTD device + if [[ ! -e /dev/mtdblock0 ]]; then + sudo modprobe nandsim + irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') + else + echo "$( sudo ls /dev/mtdblock0 )" + fi + + # Create a symlink to the virtual MTD device with the name "spi0.0" + # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name + if [[ ! -e /dev/mtdblock0 ]]; then + ln -s /dev/$virtual_mtd /dev/mtdblock0 + fi + + # Create the mount point if it doesn't exist + mkdir -p /tmp/boot + + # Mount the virtual MTD device to the mount point + mount -t jffs2 /dev/mtdblock0 /tmp/boot + + # write a file to remove + touch /tmp/boot/Mounted_MTD.txt + + echo "$( sudo ls /dev/mtd* )" + +} + +remove_virt_spi() +{ + # Unmount the virtual MTD device from the mount point + umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') + + # Remove the symlink to the virtual MTD device + rm /dev/mtdblock0 + + # Unload the nandsim and mtdblock modules to remove the virtual MTD device + sudo modprobe -r mtdblock + sudo modprobe -r nandsim + + echo "0" +} diff --git a/functions/bash-utility-master/CODE_OF_CONDUCT.md b/src/bash-utility/CODE_OF_CONDUCT.md similarity index 100% rename from functions/bash-utility-master/CODE_OF_CONDUCT.md rename to src/bash-utility/CODE_OF_CONDUCT.md diff --git a/functions/bash-utility-master/CONTRIBUTING.md b/src/bash-utility/CONTRIBUTING.md similarity index 100% rename from functions/bash-utility-master/CONTRIBUTING.md rename to src/bash-utility/CONTRIBUTING.md diff --git a/functions/bash-utility-master/LICENSE b/src/bash-utility/LICENSE similarity index 100% rename from functions/bash-utility-master/LICENSE rename to src/bash-utility/LICENSE diff --git a/functions/bash-utility-master/README.md b/src/bash-utility/README.md similarity index 100% rename from functions/bash-utility-master/README.md rename to src/bash-utility/README.md diff --git a/functions/bash-utility-master/bash_utility.sh b/src/bash-utility/bash_utility.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/bash_utility.sh rename to src/bash-utility/bash_utility.sh diff --git a/functions/bash-utility-master/bin/bashdoc.awk b/src/bash-utility/bin/bashdoc.awk old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/bin/bashdoc.awk rename to src/bash-utility/bin/bashdoc.awk diff --git a/functions/bash-utility-master/bin/generate_readme.sh b/src/bash-utility/bin/generate_readme.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/bin/generate_readme.sh rename to src/bash-utility/bin/generate_readme.sh diff --git a/functions/bash-utility-master/image/bash-utility.png b/src/bash-utility/image/bash-utility.png similarity index 100% rename from functions/bash-utility-master/image/bash-utility.png rename to src/bash-utility/image/bash-utility.png diff --git a/functions/bash-utility-master/image/logo.png b/src/bash-utility/image/logo.png similarity index 100% rename from functions/bash-utility-master/image/logo.png rename to src/bash-utility/image/logo.png diff --git a/src/bash-utility/src/array.sh b/src/bash-utility/src/array.sh new file mode 100644 index 000000000..42d5883b8 --- /dev/null +++ b/src/bash-utility/src/array.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash + +# @file Array +# @brief Functions for array operations and manipulations. + +# @description Check if item exists in the given array. +# +# @example +# array=("a" "b" "c") +# array::contains "c" ${array[@]} +# #Output +# 0 +# +# @arg $1 mixed Item to search (needle). +# @arg $2 array array to be searched (haystack). +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found in the array. +# @exitcode 2 Function missing arguments. +array::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare query="${1:-}" + shift + + for element in "${@}"; do + [[ "${element}" == "${query}" ]] && return 0 + done + + return 1 +} + +# @description Remove duplicate items from the array. +# +# @example +# array=("a" "b" "a" "c") +# printf "%s" "$(array::dedupe ${array[@]})" +# #Output +# a +# b +# c +# +# @arg $1 array Array to be deduped. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Deduplicated array. +array::dedupe() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -A arr_tmp + declare -a arr_unique + for i in "$@"; do + { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue + arr_unique+=("${i}") && arr_tmp[${i}]=x + done + printf '%s\n' "${arr_unique[@]}" +} + +# @description Check if a given array is empty. +# +# @example +# array=("a" "b" "c" "d") +# array::is_empty "${array[@]}" +# +# @arg $1 array Array to be checked. +# +# @exitcode 0 If the given array is empty. +# @exitcode 2 If the given array is not empty. +array::is_empty() { + declare -a array + local array=("$@") + if [ ${#array[@]} -eq 0 ]; then + return 0 + else + return 1 + fi +} +# @description Join array elements with a string. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s" "$(array::join "," "${array[@]}")" +# #Output +# a,b,c,d +# printf "%s" "$(array::join "" "${array[@]}")" +# #Output +# abcd +# +# @arg $1 string String to join the array elements (glue). +# @arg $2 array array to be joined with glue string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. +array::join() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare delimiter="${1}" + shift + printf "%s" "${1}" + shift + printf "%s" "${@/#/${delimiter}}" +} + +# @description Return an array with elements in reverse order. +# +# @example +# array=(1 2 3 4 5) +# printf "%s" "$(array::reverse "${array[@]}")" +# #Output +# 5 4 3 2 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The reversed array. +array::reverse() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare min=0 + declare -a array + array=("$@") + declare max=$((${#array[@]} - 1)) + + while [[ $min -lt $max ]]; do + # Swap current first and last elements + x="${array[$min]}" + array[$min]="${array[$max]}" + array[$max]="$x" + + # Move closer + ((min++, max--)) + done + printf '%s\n' "${array[@]}" +} + +# @description Returns a random item from the array. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s\n" "$(array::random_element "${array[@]}")" +# #Output +# c +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Random item out of the array. +array::random_element() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array + local array=("$@") + printf '%s\n' "${array[RANDOM % $#]}" +} + +# @description Sort an array from lowest to highest. +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# 1 +# 2 +# 4 5 +# a +# a c +# d +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout sorted array. +array::sort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort <<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Sort an array in reverse order (highest to lowest). +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# d +# a c +# a +# 4 5 +# 2 +# 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout reverse sorted array. +array::rsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort -r<<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Bubble sort an integer array from lowest to highest. +# This sort does not work on string array. +# @example +# iarr=(4 5 1 3) +# array::bsort "${iarr[@]}" +# #Output +# 1 +# 3 +# 4 +# 5 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout bubble sorted array. +array::bsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare tmp + declare arr=("$@") + for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do + for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do + if [[ ${arr[i]} -gt ${arr[j]} ]]; then + # echo $i $j ${arr[i]} ${arr[j]} + tmp=${arr[i]} + arr[i]=${arr[j]} + arr[j]=$tmp + fi + done + done + printf "%s\n" "${arr[@]}" +} + +# @description Merge two arrays. +# Pass the variable name of the array instead of value of the variable. +# @example +# a=("a" "c") +# b=("d" "c") +# array::merge "a[@]" "b[@]" +# #Output +# a +# c +# d +# c +# +# @arg $1 string variable name of first array. +# @arg $2 string variable name of second array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Merged array. +array::merge() { + [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr1=("${!1}") + declare -a arr2=("${!2}") + declare out=("${arr1[@]}" "${arr2[@]}") + printf "%s\n" "${out[@]}" +} diff --git a/src/bash-utility/src/check.sh b/src/bash-utility/src/check.sh new file mode 100644 index 000000000..2b7c1eb1d --- /dev/null +++ b/src/bash-utility/src/check.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# @file Check +# @brief Helper functions. + +# @description Check if the command exists in the system. +# +# @example +# check::command_exists "tput" +# +# @arg $1 string Command name to be searched. +# +# @exitcode 0 If the command exists. +# @exitcode 1 If the command does not exist. +# @exitcode 2 Function missing arguments. +check::command_exists() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + hash "${1}" 2> /dev/null +} + +# @description Check if the script is executed with sudo privilege. +# +# @example +# check::is_sudo +# +# @noargs +# +# @exitcode 0 If the script is executed with root privilege. +# @exitcode 1 If the script is not executed with root privilege +check::is_sudo() { + if [[ $(id -u) -ne 0 ]]; then + return 1 + fi +} diff --git a/src/bash-utility/src/collection.sh b/src/bash-utility/src/collection.sh new file mode 100644 index 000000000..9f0e244a2 --- /dev/null +++ b/src/bash-utility/src/collection.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash + +# @file Collection +# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. + +# @description Iterates over elements of collection and invokes iteratee for each element. +# Input to the function can be a pipe output, here-string or file. +# @example +# test_func(){ +# printf "print value: %s\n" "$1" +# return 0 +# } +# arr1=("a b" "c d" "a" "d") +# printf "%s\n" "${arr1[@]}" | collection::each "test_func" +# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach +# #output +# print value: a b +# print value: c d +# print value: a +# print value: d +# +# @example +# # If other function from this library is already used to process the array. +# # Then following method could be used to pass the array to the function. +# out=("$(array::dedupe "${arr1[@]}")") +# collection::each "test_func" <<< "${out[@]}" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output of iteratee function. +collection::each() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + done +} + +# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "4") +# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If iteratee function fails. +# @exitcode 2 Function missing arguments. +collection::every() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return 1 + fi + + done +} + +# @description Iterates over elements of array, returning all elements where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" +# #output +# 1 +# 2 +# 3 +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values matching the iteratee function. +collection::filter() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s\n" "${it}" + fi + done +} + +# @description Iterates over elements of collection, returning the first element where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("1" "2" "3" "a") +# check_a(){ +# [[ "$1" = "a" ]] +# } +# printf "%s\n" "${arr[@]}" | collection::find "check_a" +# #Output +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +# +# @stdout first array value matching the iteratee function. +collection::find() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s" "${it}" + return 0 + fi + done + + return 1 +} + +# @description Invokes the iteratee with each element passed as argument to the iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# opt=("-a" "-l") +# printf "%s\n" "${opt[@]}" | collection::invoke "ls" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output from the iteratee function. +collection::invoke() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a args=() + declare func="${1}" + while read -r it; do + args=("${args[@]}" "$it") + done + + eval "${func}" "${args[@]}" +} + +# @description Creates an array of values by running each element in array through iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3") +# add_one(){ +# i=${1} +# i=$(( i + 1 )) +# printf "%s\n" "$i" +# } +# printf "%s\n" "${arri[@]}" | collection::map "add_one" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output result of iteratee on value. +collection::map() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + declare out + + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + out="$("${func}")" + else + out="$("${func}" "$it")" + fi + + declare -i ret=$? + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + printf "%s\n" "${out}" + done +} + +# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" +# #Ouput +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values not matching the iteratee function. +# @see collection::filter +collection::reject() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + echo "$it" + fi + + done +} + +# @description Checks if iteratee returns true for any element of the array. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("a" "b" "3" "a") +# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If match successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +collection::some() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + + declare -i ret=$? + + if [[ $ret -eq 0 ]]; then + return 0 + fi + done + + return 1 +} diff --git a/src/bash-utility/src/date.sh b/src/bash-utility/src/date.sh new file mode 100644 index 000000000..41f071792 --- /dev/null +++ b/src/bash-utility/src/date.sh @@ -0,0 +1,744 @@ +#!/usr/bin/env bash + +# @file Date +# @brief Functions for manipulating dates. + +# @description Get current time in unix timestamp. +# +# @example +# echo "$(date::now)" +# #Output +# 1591554426 +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout current timestamp. +date::now() { + declare now + now="$(date --universal +%s)" || return $? + printf "%s" "${now}" +} + +# @description convert datetime string to unix timestamp. +# +# @example +# echo "$(date::epoc "2020-07-07 18:38")" +# #Output +# 1594143480 +# +# @arg $1 string date time in any format. +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp for specified datetime. +date::epoc() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare date + date=$(date -d "${1}" +"%s") || return $? + printf "%s" "${date}" +} + +# @description Add number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days_from "1594143480")" +# #Output +# 1594229880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months_from "1594143480")" +# #Output +# 1596821880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years_from "1594143480")" +# #Output +# 1625679480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::add_weeks_from "1594143480")" +# #Output +# 1594748280 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours_from "1594143480")" +# #Output +# 1594147080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes_from "1594143480")" +# #Output +# 1594143540 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds_from "1594143480")" +# #Output +# 1594143481 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days "1")" +# #Output +# 1591640826 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months "1")" +# #Output +# 1594146426 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years "1")" +# #Output +# 1623090426 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_weeks "1")" +# #Output +# 1592159226 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours "1")" +# #Output +# 1591558026 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes "1")" +# #Output +# 1591554486 +# +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds "1")" +# #Output +# 1591554427 +# +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days_from "1594143480")" +# #Output +# 1594057080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months_from "1594143480")" +# #Output +# 1591551480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years_from "1594143480")" +# #Output +# 1562521080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks_from "1594143480")" +# #Output +# 1593538680 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours_from "1594143480")" +# #Output +# 1594139880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes_from "1594143480")" +# #Output +# 1594143420 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds_from "1594143480")" +# #Output +# 1594143479 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days "1")" +# #Output +# 1588876026 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months "1")" +# #Output +# 1559932026 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years "1")" +# #Output +# 1591468026 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks "1")" +# #Output +# 1590949626 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours "1")" +# #Output +# 1591550826 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes "1")" +# #Output +# 1591554366 +# +# @arg $1 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds "1")" +# #Output +# 1591554425 +# +# @arg $1 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Format unix timestamp to human readable format. +# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. +# +# @example +# echo echo "$(date::format "1594143480")" +# #Output +# 2020-07-07 18:38:00 +# +# @arg $1 int unix timestamp. +# @arg $2 string format control characters based on `date` command (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate time string. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +date::format() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp format out + timestamp="${1}" + format="${2:-"%F %T"}" + out="$(date -d "@${timestamp}" +"${format}")" || return $? + printf "%s" "${out}" + +} diff --git a/src/bash-utility/src/debug.sh b/src/bash-utility/src/debug.sh new file mode 100644 index 000000000..82828734f --- /dev/null +++ b/src/bash-utility/src/debug.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# @file Debug +# @brief Functions to facilitate debugging scripts. + +# @description Prints the content of array as key value pair for easier debugging. +# Pass the variable name of the array instead of value of the variable. +# @example +# array=(foo bar baz) +# printf "Array\n" +# printarr "array" +# declare -A assoc_array +# assoc_array=([foo]=bar [baz]=foobar) +# printf "Assoc Array\n" +# printarr "assoc_array" +# #Output +# Array +# 0 = foo +# 1 = bar +# 2 = baz +# Assoc Array +# baz = foobar +# foo = bar +# +# @arg $1 string variable name of the array. +# +# @stdout Formatted key value of array. +debug::print_array() { + declare -n __arr="$1" + for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done +} + +# @description Function to print ansi escape sequence as is. +# This function helps debug ansi escape sequence in text by displaying the escape codes. +# +# @example +# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" +# debug::print_ansi "$txt" +# #Output +# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m +# +# @arg $1 string input with ansi escape sequence. +# +# @stdout Ansi escape sequence printed in output as is. +debug::print_ansi() { + #echo $(tr -dc '[:print:]'<<<$1) + printf "%s\n" "${1//$'\e'/\\e}" + +} diff --git a/src/bash-utility/src/file.sh b/src/bash-utility/src/file.sh new file mode 100644 index 000000000..71afd8476 --- /dev/null +++ b/src/bash-utility/src/file.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash + +# @file File +# @brief Functions for handling files. + +# @description Create temporary file. +# Function creates temporary file with random name. The temporary file will be deleted when script finishes. +# +# @example +# echo "$(file::make_temp_file)" +# #Output +# tmp.vgftzy +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp file. +# +# @stdout file name of temporary file created. +file::make_temp_file() { + declare temp_file + type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } + trap 'rm -f "${temp_file}"' EXIT + printf "%s" "${temp_file}" +} + +# @description Create temporary directory. +# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. +# +# @example +# echo "$(utility::make_temp_dir)" +# #Output +# tmp.rtfsxy +# +# @arg $1 string Temporary directory prefix +# @arg $2 string Flag to auto remove directory on exit trap (true) +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp directory. +# @exitcode 2 Missing arguments. +# +# @stdout directory name of temporary directory created. +file::make_temp_dir() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare temp_dir prefix="${1}" trap_rm="${2}" + temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") + if [[ -n "${trap_rm}" ]]; then + trap 'rm -rf "${temp_dir}"' EXIT + fi + printf "%s" "${temp_dir}" +} + +# @description Get only the filename from string path. +# +# @example +# echo "$(file::name "/path/to/test.md")" +# #Output +# test.md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout name of the file with extension. +file::name() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf "%s" "${1##*/}" +} + +# @description Get the basename of file from file name. +# +# @example +# echo "$(file::basename "/path/to/test.md")" +# #Output +# test +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout basename of the file. +file::basename() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare file basename + file="${1##*/}" + basename="${file%.*}" + + printf "%s" "${basename}" +} + +# @description Get the extension of file from file name. +# +# @example +# echo "$(file::extension "/path/to/test.md")" +# #Output +# md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 1 If no extension is found in the filename. +# @exitcode 2 Function missing arguments. +# +# @stdout extension of the file. +file::extension() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare file extension + file="${1##*/}" + extension="${file##*.}" + [[ "${file}" = "${extension}" ]] && return 1 + + printf "%s" "${extension}" +} + +# @description Get directory name from file path. +# +# @example +# echo "$(file::dirname "/path/to/test.md")" +# #Output +# /path/to +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout directory path. +file::dirname() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare tmp=${1:-.} + + [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } + tmp="${tmp%%"${tmp##*[!/]}"}" + + [[ ${tmp} != */* ]] && { printf '.\n' && return; } + tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" + + printf '%s' "${tmp:-/}" +} + +# @description Get absolute path of file or directory. +# +# @example +# file::full_path "../path/to/file.md" +# #Output +# /home/labbots/docs/path/to/file.md +# +# @arg $1 string relative or absolute path to file/direcotry. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# +# @stdout Absolute path to file/directory. +file::full_path() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare input="${1}" + if [[ -f ${input} ]]; then + printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" + elif [[ -d ${input} ]]; then + printf "%s\n" "$(cd "${input}" && pwd)" + else + return 1 + fi +} + +# @description Get mime type of provided input. +# +# @example +# file::mime_type "../src/file.sh" +# #Output +# application/x-shellscript +# +# @arg $1 string relative or absolute path to file/directory. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# @exitcode 3 If file or mimetype command not found in system. +# +# @stdout mime type of file/directory. +file::mime_type() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare mime_type + if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then + if type -p mimetype &> /dev/null; then + mime_type=$(mimetype --output-format %m "${1}") + elif type -p file &> /dev/null; then + mime_type=$(file --brief --mime-type "${1}") + else + return 3 + fi + else + return 1 + fi + printf "%s" "${mime_type}" +} + +# @description Search if a given pattern is found in file. +# +# @example +# file::contains_text "./file.sh" "^[ @[:alpha:]]*" +# file::contains_text "./file.sh" "@file" +# #Output +# 0 +# +# @arg $1 string relative or absolute path to file/directory. +# @arg $2 string search key or regular expression. +# +# @exitcode 0 If given search parameter is found in file. +# @exitcode 1 If search paramter not found in file. +# @exitcode 2 Function missing arguments. +file::contains_text() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + declare -r file="$1" + declare -r text="$2" + grep -q "$text" "$file" +} diff --git a/src/bash-utility/src/format.sh b/src/bash-utility/src/format.sh new file mode 100644 index 000000000..7dceb1d59 --- /dev/null +++ b/src/bash-utility/src/format.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash + +# @file Format +# @brief Functions to format provided input. + +# @internal +# @description Initialisation script when the code is sourced. +# +# @noargs +__init(){ +_check_terminal_window_size +} + +# @internal +# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. +# +# @noargs +_check_terminal_window_size() { + shopt -s checkwinsize && (: && :) + trap 'shopt -s checkwinsize; (:;:)' SIGWINCH +} +# @description Format seconds to human readable format. +# +# @example +# echo "$(format::human_readable_seconds "356786")" +# #Output +# 4 days 3 hours 6 minute(s) and 26 seconds +# +# @arg $1 int number of seconds. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +format::human_readable_seconds() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare T="${1}" + declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" + [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" + [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" + [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" + [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' + printf '%d seconds\n' "${SEC}" +} + +# @description Format bytes to human readable format. +# +# @example +# echo "$(format::bytes_to_human "2250")" +# #Output +# 2.19 KB +# +# @arg $1 int size in bytes. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted file size string. +format::bytes_to_human() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) + while ((b > 1024)); do + d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" + b=$((b / 1024)) && ((s++)) + done + printf "%s\n" "${b}${d} ${S[${s}]}" +} + +# @description Remove Ansi escape sequences from given text. +# +# @example +# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" +# #Output +# This is bold red text.This is green text. +# +# @arg $1 string Input text to be ansi stripped. +# +# @exitcode 0 If successful. +# +# @stdout Ansi stripped text. +format::strip_ansi() { + declare tmp esc tpa re + tmp="${1}" + esc=$(printf "\x1b") + tpa=$(printf "\x28") + re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" + while [[ "${tmp}" =~ $re ]]; do + tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" + done + printf "%s" "${tmp}" +} + +# @description Prints the given text to centre of terminal. +# +# @example +# format::text_center "This text is in centre of the terminal." "-" +# +# @arg $1 string Text to be printed. +# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::text_center() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare input="${1}" symbol="${2:- }" filler out no_ansi_out + no_ansi_out=$(format::strip_ansi "$input") + declare -i str_len=${#no_ansi_out} + declare -i filler_len="$(((COLUMNS - str_len) / 2))" + + [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" + for ((i = 0; i < filler_len; i++)); do + filler+="${symbol}" + done + + out="${filler}${input}${filler}" + [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" + printf "%s" "${out}" +} + +# @description Format String to print beautiful report. +# +# @example +# format::report "Initialising mission state" "Success" +# #Output +# Initialising mission state ....................................................................[ Success ] +# +# @arg $1 string Text to be printed on the left. +# @arg $2 string Text to be printed within the square brackets. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::report() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare symbol="." to_print y hl hlout out + declare input1="${1} " input2="${2}" + input2="[ $input2 ]" + to_print="$((COLUMNS * 60 / 100))" + y=$(( to_print - ( ${#input1} + ${#input2} ) )) + hl="$(printf '%*s' $y '')" + hlout=${hl// /${symbol}} + out="${input1}${hlout}${input2}" + printf "%s\n" "${out}" +} + +# @description Trim given text to width of the terminal window. +# +# @example +# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." +# #Output +# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. +# +# @arg $1 string Text of first sentence. +# @arg $2 string Text of second sentence (optional). +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout trimmed text. +format::trim_text_to_term() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare to_print out input1="$1" input2="$2" + if [[ $# = 1 ]]; then + to_print="$((COLUMNS * 93 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } + else + to_print="$((COLUMNS * 40 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } + to_print="$((COLUMNS * 53 / 100))" + { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } + fi + printf "%s" "$out" +} + +__init diff --git a/src/bash-utility/src/interaction.sh b/src/bash-utility/src/interaction.sh new file mode 100644 index 000000000..910b60e1c --- /dev/null +++ b/src/bash-utility/src/interaction.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# @file Interaction +# @brief Functions to enable interaction with the user. + +# @description Prompt yes or no question to the user. +# +# @example +# interaction::prompt_yes_no "Are you sure to proceed" "yes" +# #Output +# Are you sure to proceed (y/n)? [y] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer \[yes/no\] (optional). +# +# @exitcode 0 If user responds with yes. +# @exitcode 1 If user responds with no. +# @exitcode 2 Function missing arguments. +interaction::prompt_yes_no() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare def_arg response + def_arg="" + response="" + + case "${2}" in + [yY] | [yY][eE][sS]) + def_arg=y + ;; + [nN] | [nN][oO]) + def_arg=n + ;; + esac + + while :; do + printf "%s (y/n)? " "${1}" + [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -z "${response}" ]] && response="${def_arg}" + + case "${response}" in + [yY] | [yY][eE][sS]) + response=y + break + ;; + [nN] | [nN][oO]) + response=n + break + ;; + *) + response="" + ;; + esac + done + + [[ "${response}" = 'y' ]] && return 0 || return 1 +} + +# @description Prompt question to the user. +# +# @example +# interaction::prompt_response "Choose directory to install" "/home/path" +# #Output +# Choose directory to install? [/home/path] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer (optional). +# +# @exitcode 0 If user responds with answer. +# @exitcode 2 Function missing arguments. +# +# @stdout User entered answer to the question. +interaction::prompt_response() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare def_arg response + response="" + def_arg="${2}" + + while :; do + printf "%s ? " "${1}" + [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -n "${response}" ]] && break + + if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then + response="${def_arg}" + break + fi + done + + [[ "${response}" = "-" ]] && response="" + + printf "%s" "${response}" +} diff --git a/src/bash-utility/src/json.sh b/src/bash-utility/src/json.sh new file mode 100644 index 000000000..73476618e --- /dev/null +++ b/src/bash-utility/src/json.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# @file Json +# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. + +# @description Extract value from json based on key and position. +# Input to the function can be a pipe output, here-string or file. +# @example +# json::get_value "id" "1" < json_file +# json::get_value "id" <<< "${json_var}" +# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" +# +# @arg $1 string id of the field to fetch. +# @arg $2 int position of value to extract.Defaults to 1.(optional) +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string value of extracted key. +json::get_value() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare LC_ALL=C num="${2:-1}" + grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p +} diff --git a/src/bash-utility/src/misc.sh b/src/bash-utility/src/misc.sh new file mode 100644 index 000000000..fca9a75bf --- /dev/null +++ b/src/bash-utility/src/misc.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# @file Miscellaneous +# @brief Set of miscellaneous helper functions. + +# @internal +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +_is_terminal() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Check if internet connection is available. +# +# @example +# misc::check_internet_connection +# +# @noargs +# +# @exitcode 0 If script can connect to internet. +# @exitcode 1 If script cannot access internet. +misc::check_internet_connection() { + declare check_internet + if _is_terminal; then + check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" + else + check_internet="$(curl --compressed -Is google.com -m 10)" + fi + if [[ -z ${check_internet} ]]; then + return 1 + fi +} + +# @description Get list of process ids based on process name. +# +# @example +# misc::get_pid "chrome" +# #Ouput +# 25951 +# 26043 +# 26528 +# 26561 +# +# @arg $1 Name of the process to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout list of process ids. +misc::get_pid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + pgrep "${1}" +} + +# @description Get user id based on username. +# +# @example +# misc::get_uid "labbots" +# #Ouput +# 1000 +# +# @arg $1 username to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string uid for the username. +misc::get_uid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + user_id=$(id "${1}" 2> /dev/null) + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + printf "No user found with username: %s" "${1}\n" + return 1 + fi + + printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' + + unset user_id +} + +# @description Generate random uuid. +# +# @example +# misc::generate_uuid +# #Ouput +# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c +# +# @noargs +# +# @exitcode 0 If match successful. +# +# @stdout random generated uuid. +misc::generate_uuid() { + C="89ab" + + for ((N=0;N<16;++N)); do + B="$((RANDOM%256))" + + case "$N" in + 6) printf '4%x' "$((B%16))" ;; + 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; + + 3|5|7|9) + printf '%02x-' "$B" + ;; + + *) + printf '%02x' "$B" + ;; + esac + done + + printf '\n' +} diff --git a/src/bash-utility/src/os.sh b/src/bash-utility/src/os.sh new file mode 100644 index 000000000..5cfecfc78 --- /dev/null +++ b/src/bash-utility/src/os.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +# @file Operating System +# @brief Functions to detect Operating system and version. + +# @description Identify the OS the function is run on. +# +# @noargs +# +# @example +# os::detect_os +# #Output +# linux +# +# @exitcode 0 If OS is successfully detected. +# @exitcode 1 If unable to detect OS. +# +# @stdout Operating system name (linux, mac or windows). +os::detect_os() { + declare uname os + uname=$(command -v uname) + + case $("${uname}" | tr '[:upper:]' '[:lower:]') in + linux*) + os="linux" + ;; + darwin*) + os="mac" + ;; + msys* | cygwin* | mingw* | nt | win*) + # or possible 'bash on windows' + os="windows" + ;; + *) + return 1 + ;; + esac + printf "%s" "${os}" +} + +# @description Identify the distribution flavour of linux. +# +# @noargs +# +# @example +# os::detect_linux_distro +# #Output +# ubuntu +# @exitcode 0 If Linux distro is successfully detected. +# @exitcode 1 If unable to detect OS distro. +# +# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). +os::detect_linux_distro() { + declare distro + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro="${NAME}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro=$(lsb_release -si) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro="${DISTRIB_ID}" + elif [[ -f /etc/debian_version ]]; then + # Older Debian/Ubuntu/etc. + distro="debian" + elif [[ -f /etc/SuSe-release ]]; then + # Older SuSE/etc. + distro="suse" + elif [[ -f /etc/redhat-release ]]; then + # Older Red Hat, CentOS, etc. + distro="redhat" + else + return 1 + fi + printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' +} + +# @description Identify the Linux version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 20.04 +# +# @exitcode 0 If Linux version is successfully detected. +# @exitcode 1 If unable to detect Linux version. +# +# @stdout Linux OS version number (18.04, 20.04, etc.,). +os::detect_linux_version() { + declare distro_version + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro_version="${VERSION_ID}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro_version=$(lsb_release -sr) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro_version="${DISTRIB_RELEASE}" + else + return 1 + fi + printf "%s" "${distro_version}" +} + +# @description Identify the MacOS version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 10.15.7 +# @exitcode 0 If MacOS version is successfully detected. +# @exitcode 1 If unable to detect MacOS version. +# +# @stdout MacOS version number (10.15.6, etc.,) +os::detect_mac_version() { + if [[ "$(os::detect_os)" = "mac" ]]; then + declare mac_version + mac_version="$(sw_vers -productVersion)" + printf "%s" "${mac_version}" + else + return 1 + fi +} diff --git a/src/bash-utility/src/string.sh b/src/bash-utility/src/string.sh new file mode 100644 index 000000000..aa522cb55 --- /dev/null +++ b/src/bash-utility/src/string.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash + +# @file String +# @brief Functions for string operations and manipulations. + +# @description Strip whitespace from the beginning and end of a string. +# +# @example +# echo "$(string::trim " Hello World! ")" +# #Output +# Hello World! +# +# @arg $1 string The string to be trimmed. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The trimmed string. +string::trim() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + : "${1#"${1%%[![:space:]]*}"}" + : "${_%"${_##*[![:space:]]}"}" + printf '%s\n' "$_" +} + +# @description Split a string to array by a delimiter. +# +# @example +# array=( $(string::split "a,b,c" ",") ) +# printf "%s" "$(string::split "Hello!World" "!")" +# #Output +# Hello +# World +# +# @arg $1 string The input string. +# @arg $2 string The delimiter string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. +string::split() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr=() + IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" + printf '%s\n' "${arr[@]}" +} + +# @description Strip characters from the beginning of a string. +# +# @example +# echo "$(string::lstrip "Hello World!" "He")" +# #Output +# llo World! +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::lstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1##$2}" +} + +# @description Strip characters from the end of a string. +# +# @example +# echo "$(string::rstrip "Hello World!" "d!")" +# #Output +# Hello Worl +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::rstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1%%$2}" +} + +# @description Make a string lowercase. +# +# @example +# echo "$(string::to_lower "HellO")" +# #Output +# hello +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the lowercased string. +string::to_lower() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1,,}" + else + printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' + fi +} + +# @description Make a string all uppercase. +# +# @example +# echo "$(string::to_upper "HellO")" +# #Output +# HELLO +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the uppercased string. +string::to_upper() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1^^}" + else + printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' + fi +} + +# @description Check whether the search string exists within the input string. +# +# @example +# string::contains "Hello World!" "lo" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_contains hello he + [[ "${1}" == *${2}* ]] +} + +# @description Check whether the input string starts with key string. +# +# @example +# string::starts_with "Hello World!" "He" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::starts_with() { + # Usage: string_starts_with hello he + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + [[ "${1}" == ${2}* ]] +} + +# @description Check whether the input string ends with key string. +# +# @example +# string::ends_with "Hello World!" "d!" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::ends_with() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_ends_wit hello lo + [[ "${1}" == *${2} ]] +} + +# @description Check whether the input string matches the given regex. +# +# @example +# string::regex "HELLO" "^[A-Z]*$" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::regex() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${1} =~ ${2} ]]; then + return 0 + else + return 1 + fi + +} diff --git a/src/bash-utility/src/terminal.sh b/src/bash-utility/src/terminal.sh new file mode 100644 index 000000000..d73331d7a --- /dev/null +++ b/src/bash-utility/src/terminal.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# @file Terminal +# @brief Set of useful terminal functions. + +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +terminal::is_term() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Detect profile rc file for zsh and bash of current script running user. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +# +# @stdout path to the profile file. +terminal::detect_profile() { + declare CURRENT_SHELL="${SHELL##*/}" + case "${CURRENT_SHELL}" in + 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; + 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; + *) if [[ -f "${HOME}/.profile" ]]; then + DETECTED_PROFILE="${HOME}/.profile" + else + printf "No compaitable shell file\n" && exit 1 + fi ;; + esac + printf "%s\n" "${DETECTED_PROFILE}" +} + +# @description Clear the output in terminal on the specified line number. +# This function clears line only on terminal. +# +# @arg $1 Line number to clear. Defaults to 1. (optional) +# +# @exitcode 0 If script is run on terminal. +# +# @stdout clear line ansi code. +terminal::clear_line() { + if terminal::is_term; then + declare line=${1:-1} + printf "\033[%sA\033[2K" "${line}" + fi +} diff --git a/src/bash-utility/src/validation.sh b/src/bash-utility/src/validation.sh new file mode 100644 index 000000000..37fde1cbc --- /dev/null +++ b/src/bash-utility/src/validation.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash + +# @file Validation +# @brief Functions to perform validation on given data. + +# @description Validate whether a given input is a valid email address or not. +# +# @example +# test='test@gmail.com' +# validation::email "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string input email address to validate. +# +# @exitcode 0 If provided input is an email address. +# @exitcode 1 If provided input is not an email address. +# @exitcode 2 Function missing arguments. +validation::email() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare email_re + email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" + [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 +} + +# @description Validate whether a given input is a valid IP V4 address. +# +# @example +# ips=' +# 4.2.2.2 +# a.b.c.d +# 192.168.1.1 +# 0.0.0.0 +# 255.255.255.255 +# 255.255.255.256 +# 192.168.0.1 +# 192.168.0 +# 1234.123.123.123 +# 0.192.168.1 +# ' +# for ip in $ips; do +# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi +# printf "%-20s: %s\n" "$ip" "$stat" +# done +# #Output +# 4.2.2.2 : good +# a.b.c.d : bad +# 192.168.1.1 : good +# 0.0.0.0 : good +# 255.255.255.255 : good +# 255.255.255.256 : bad +# 192.168.0.1 : good +# 192.168.0 : bad +# 1234.123.123.123 : bad +# 0.192.168.1 : good +# +# @arg $1 string input IPv4 address. +# +# @exitcode 0 If provided input is a valid IPv4. +# @exitcode 1 If provided input is not a valid IPv4. +# @exitcode 2 Function missing arguments. +validation::ipv4() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare ip="${1}" + declare IFS=. + # shellcheck disable=SC2206 + declare -a a=($ip) + [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 + # Test values of quads + declare quad + for quad in {0..3}; do + [[ "${a[$quad]}" -gt 255 ]] && return 1 + done + return 0 +} + +# @description Validate whether a given input is a valid IP V6 address. +# +# @example +# ips=' +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 +# fe80::1ff:fe23:4567:890a +# fe80::1ff:fe23:4567:890a%eth2 +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar +# fezy::1ff:fe23:4567:890a +# :: +# 2001:db8:: +# ' +# for ip in $ips; do +# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi +# printf "%-50s= %s\n" "$ip" "$stat" +# done +# #Output +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good +# fe80::1ff:fe23:4567:890a = good +# fe80::1ff:fe23:4567:890a%eth2 = good +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad +# fezy::1ff:fe23:4567:890a = bad +# :: = good +# 2001:db8:: = good +# +# @arg $1 string input IPv6 address. +# +# @exitcode 0 If provided input is a valid IPv6. +# @exitcode 1 If provided input is not a valid IPv6. +# @exitcode 2 Function missing arguments. +validation::ipv6() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare ip="${1}" + declare 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]))$" + + [[ "${ip}" =~ $re ]] && return 0 || return 1 +} + +# @description Validate if given variable is entirely alphabetic characters. +# +# @example +# test='abcABC' +# validation::alpha "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is only alpha characters. +# @exitcode 1 If input contains any non alpha characters. +# @exitcode 2 Function missing arguments. +validation::alpha() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable contains only alpha-numeric characters. +# +# @example +# test='abc123' +# validation::alpha_num "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is an alpha-numeric. +# @exitcode 1 If input is not an alpha-numeric. +# @exitcode 2 Function missing arguments. +validation::alpha_num() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alnum:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. +# +# @example +# test='abc-ABC_cD' +# validation::alpha_dash "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is valid. +# @exitcode 1 If input the input is not valid. +# @exitcode 2 Function missing arguments. +validation::alpha_dash() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]_-]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. +# +# @arg $1 string Version number to check (eg: 1.0.1) +# $arg $2 string Version number to check (eg: 1.0.1) +# +# @example +# test='abc-ABC_cD' +# validation::version_comparison "12.0.1" "12.0.1" +# echo $? +# #Output +# 0 +# +# @exitcode 0 version number is equal. +# @exitcode 1 $1 version number is greater than $2. +# @exitcode 2 $1 version number is less than $2. +# @exitcode 3 Function is missing required arguments. +# @exitcode 4 Provided input argument is in invalid format. +validation::version_comparison() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 + + declare regex="^[.0-9]*$" + ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 + ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 + + if [[ "$1" == "$2" ]]; then + return 0 + fi + declare IFS=. + declare -a ver1 ver2 + read -r -a ver1 <<<"${1}" + read -r -a ver2 <<<"${2}" + # fill empty fields in ver1 with zeros + for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do + ver1[i]=0 + done + for ((i = 0; i < ${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + # fill empty fields in ver2 with zeros + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 + fi + done + return 0 +} diff --git a/src/bash-utility/src/variable.sh b/src/bash-utility/src/variable.sh new file mode 100644 index 000000000..67e9fab55 --- /dev/null +++ b/src/bash-utility/src/variable.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# @file Variable +# @brief Functions for handling variables. + +# @description Check if given variable is array. +# Pass the variable name instead of value of the variable. +# +# @example +# arr=("a" "b" "c") +# variable::is_array "arr" +# #Output +# 0 +# +# @arg $1 string name of the variable to check. +# +# @exitcode 0 If input is array. +# @exitcode 1 If input is not an array. +variable::is_array() { + if [[ -z "${1}" ]]; then + return 1 + else + declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 + fi + return 1 +} + +# @description Check if given variable is a number. +# +# @example +# variable::is_numeric "1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is number. +# @exitcode 1 If input is not a number. +variable::is_numeric() { + declare re='^[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is an integer. +# +# @example +# variable::is_int "+1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is an integer. +# @exitcode 1 If input is not an integer. +variable::is_int() { + declare re='^[+-]?[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a float. +# +# @example +# variable::is_float "+1234.0" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a float. +# @exitcode 1 If input is not a float. +variable::is_float() { + declare re='^[+-]?[0-9]+.?[0-9]*$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a boolean. +# +# @example +# variable::is_bool "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a boolean. +# @exitcode 1 If input is not a boolean. +variable::is_bool() { + [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 +} + +# @description Check if given variable is a true. +# +# @example +# variable::is_true "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is true. +# @exitcode 1 If input is not true. +variable::is_true() { + [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 +} + +# @description Check if given variable is false. +# +# @example +# variable::is_false "false" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is false. +# @exitcode 1 If input is not false. +variable::is_false() { + [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 +} + +# @description Check if given variable is empty or null. +# +# @example +# test='' +# variable::is_empty_or_null $test +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is empty or null. +# @exitcode 1 If input is not empty or null. +variable::is_empty_or_null() { + [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 +} diff --git a/src/configng/README.md b/src/configng/README.md new file mode 100644 index 000000000..1653df4c9 --- /dev/null +++ b/src/configng/README.md @@ -0,0 +1,62 @@ +# configng +This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility) +embedded in this project. This allows for functional programming in Bash and also modernizes +the monolithic nature of armbian-config. Error handling and validation are also included. +The idea is to provide an API in Bash that can be called from a TUI, GUI or CLI. Please +follow the coding standards which follow Bash Utility functions. + +Why Bash? Well, because it's going to be in every distribution. Striped down distributions +may not include Python, C/C++, etc. build/runtime environments + +## Quick start +* `sudo apt install git` +* `cd ~/` +* `git clone https://github.com/armbian/configng.git` +* `cd ~/configng/test` +* `sudo ./cpu_test.sh` +If all goes well you should see all the functions in cpu.sh called and output diaplayed. + +## Coding standards +[Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, +but fundementally look at the code in Bash Utility: +``` +# @description Strip characters from the beginning of a string. +# +# @example +# echo "$(string::lstrip "Hello World!" "He")" +# #Output +# llo World! +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::lstrip() { +[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 +printf '%s\n' "${1##$2}" +} +``` + +Functions should follow filename::func_name style. Then you can tell just from the name which +file the function is located in. Return codes should also follow a similar pattern: +* 0 Successful +* 1 Not found +* 2 Function missing arguments +* 3-255 all other errors + +Validate values: +``` +# Validate minimum frequency is <= maximum frequency +[ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 +``` + +Return values should use stdout: +``` +# Return value +printf '%s\n' "$(cat $file)" +``` + +Only use sudo when needed and never run as root! diff --git a/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md b/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..461c926b9 --- /dev/null +++ b/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at + +For answers to common questions about this code of conduct, see + + +[homepage]: https://www.contributor-covenant.org diff --git a/src/configng/functions/bash-utility-master/CONTRIBUTING.md b/src/configng/functions/bash-utility-master/CONTRIBUTING.md new file mode 100644 index 000000000..aa401df88 --- /dev/null +++ b/src/configng/functions/bash-utility-master/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing to Bash-Utility + +:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: +The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Table of Contents +- [Code Contributions](#code-contributions) +- [Code Guidelines](#code-guidelines) + - [Styleguide](#styleguide) + - [Bashdoc guideline](#bashdoc-guideline) +- [Documentation](#documentation) +- [Commit Guidelines](#commit-guidelines) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Contact](#contact) + +## Code Contributions + +Great, the more, the merrier. + +Sane code contributions are always welcome, whether to the code or documentation. + +Before making a pull request, make sure to follow below guidelines: + +### Code Guidelines + +#### Styleguide + +- Variable names must be meaningful and self-documenting. +- Long variable names must be structured by underscores to improve legibility. +- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE) +- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data) +- Variable names can be alphanumeric with underscores. No special characters in variable names. +- Variables name must not start with number. +- Variables within function must be declared. So the scope of variable is restricted to the function. +- Avoid accessing global variables within functions. +- Function names must be all lower case with underscores to seperate words (snake_case). +- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`) +- Try using bash builtins and string substitution as much as possible. +- Use printf everywhere instead of echo. +- Before adding a new logic, be sure to check the existing code. +- Make sure to add the function in appropriate section based on its operation. +- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command: + + ```shell + shfmt upload.sh + ``` + + The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags. + You can also install shfmt for various editors, refer their repo for information. + Note: This is strictly necessary to maintain consistency, do not skip. + +- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason. + +#### Bashdoc guideline + +The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is +properly generated by the generator. + +Follow the below bashdoc template to add function introductory comment. + +```bash +# @description Multiline description goes here and +# there +# +# @example +# sample::function a b c +# echo 123 +# +# @arg $1 string Some arg. +# @arg $2 any Rest of arguments. +# +# @noargs +# +# @exitcode 0 If successfull. +# @exitcode >0 On failure +# @exitcode 5 On some error. +# +# @stdout Path to something. +# +# @see sample::other_function(() +sample::function() { +} +``` + +- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used. +- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment. +- Make sure to document the exitcode emitted by the function. +- If the function is similar to other function add a reference to function using @see tag. + +### Documentation + +- Refrain from making unnecessary newlines or whitespace. +- Use pure markdown as much as possible, html is accepted but shouldn't be a priority. +- The markdown must pass RemarkLint checks. +- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc. + + ```bash + ./bin/generate_readme.sh -f README.md -s src/ + ``` + +### Commit Guidelines + +It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted. + +It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message. + +The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation. + +Before committing check for unnecessary whitespace with `git diff --check`. + +### Pull Request Guidelines + +The following guidelines will increase the likelihood that your pull request will get accepted: + +- Follow the commit and code guidelines. +- Keep the patches on topic and focused. +- Try to avoid unnecessary formatting and clean-up where reasonable. + +A pull request should contain the following: + +- At least one commit (all of which should follow the Commit Guidelines). +- Title that summarises the issue/feature. +- Description that briefly summarises the changes. + +After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread. + +## Contact + +For further inquiries, you can contact the developer by opening an issue on the repository. diff --git a/src/configng/functions/bash-utility-master/LICENSE b/src/configng/functions/bash-utility-master/LICENSE new file mode 100644 index 000000000..99dd0836b --- /dev/null +++ b/src/configng/functions/bash-utility-master/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 labbots + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/configng/functions/bash-utility-master/README.md b/src/configng/functions/bash-utility-master/README.md new file mode 100644 index 000000000..9bc1f9484 --- /dev/null +++ b/src/configng/functions/bash-utility-master/README.md @@ -0,0 +1,3026 @@ +

Bash Utility

+ +

+Stars +License + +

+

+Gh-pages Status +Website +

+

+Total number of Library functions +

+

+ +

+Bash library which provides utility functions and helpers for functional programming in Bash. + +Detailed documentation is available at + + + +## Table of Contents + +- [Installation](#installation) + - [Method 1 - Git Submodules](#method-1---git-submodules) + - [Method 2 - Git Clone](#method-2---git-clone) + - [Method 3 - Direct Download](#method-3---direct-download) +- [Usage](#usage) +- [Array](#array) + - [array::contains()](#arraycontains) + - [array::dedupe()](#arraydedupe) + - [array::is_empty()](#arrayis_empty) + - [array::join()](#arrayjoin) + - [array::reverse()](#arrayreverse) + - [array::random_element()](#arrayrandom_element) + - [array::sort()](#arraysort) + - [array::rsort()](#arrayrsort) + - [array::bsort()](#arraybsort) + - [array::merge()](#arraymerge) +- [Check](#check) + - [check::command_exists()](#checkcommand_exists) + - [check::is_sudo()](#checkis_sudo) +- [Collection](#collection) + - [collection::each()](#collectioneach) + - [collection::every()](#collectionevery) + - [collection::filter()](#collectionfilter) + - [collection::find()](#collectionfind) + - [collection::invoke()](#collectioninvoke) + - [collection::map()](#collectionmap) + - [collection::reject()](#collectionreject) + - [collection::some()](#collectionsome) +- [Date](#date) + - [date::now()](#datenow) + - [date::epoc()](#dateepoc) + - [date::add_days_from()](#dateadd_days_from) + - [date::add_months_from()](#dateadd_months_from) + - [date::add_years_from()](#dateadd_years_from) + - [date::add_weeks_from()](#dateadd_weeks_from) + - [date::add_hours_from()](#dateadd_hours_from) + - [date::add_minutes_from()](#dateadd_minutes_from) + - [date::add_seconds_from()](#dateadd_seconds_from) + - [date::add_days()](#dateadd_days) + - [date::add_months()](#dateadd_months) + - [date::add_years()](#dateadd_years) + - [date::add_weeks()](#dateadd_weeks) + - [date::add_hours()](#dateadd_hours) + - [date::add_minutes()](#dateadd_minutes) + - [date::add_seconds()](#dateadd_seconds) + - [date::sub_days_from()](#datesub_days_from) + - [date::sub_months_from()](#datesub_months_from) + - [date::sub_years_from()](#datesub_years_from) + - [date::sub_weeks_from()](#datesub_weeks_from) + - [date::sub_hours_from()](#datesub_hours_from) + - [date::sub_minutes_from()](#datesub_minutes_from) + - [date::sub_seconds_from()](#datesub_seconds_from) + - [date::sub_days()](#datesub_days) + - [date::sub_months()](#datesub_months) + - [date::sub_years()](#datesub_years) + - [date::sub_weeks()](#datesub_weeks) + - [date::sub_hours()](#datesub_hours) + - [date::sub_minutes()](#datesub_minutes) + - [date::sub_seconds()](#datesub_seconds) + - [date::format()](#dateformat) +- [Debug](#debug) + - [debug::print_array()](#debugprint_array) + - [debug::print_ansi()](#debugprint_ansi) +- [File](#file) + - [file::make_temp_file()](#filemake_temp_file) + - [file::make_temp_dir()](#filemake_temp_dir) + - [file::name()](#filename) + - [file::basename()](#filebasename) + - [file::extension()](#fileextension) + - [file::dirname()](#filedirname) + - [file::full_path()](#filefull_path) + - [file::mime_type()](#filemime_type) + - [file::contains_text()](#filecontains_text) +- [Format](#format) + - [format::human_readable_seconds()](#formathuman_readable_seconds) + - [format::bytes_to_human()](#formatbytes_to_human) + - [format::strip_ansi()](#formatstrip_ansi) + - [format::text_center()](#formattext_center) + - [format::report()](#formatreport) + - [format::trim_text_to_term()](#formattrim_text_to_term) +- [Interaction](#interaction) + - [interaction::prompt_yes_no()](#interactionprompt_yes_no) + - [interaction::prompt_response()](#interactionprompt_response) +- [Json](#json) + - [json::get_value()](#jsonget_value) +- [Miscellaneous](#miscellaneous) + - [misc::check_internet_connection()](#misccheck_internet_connection) + - [misc::get_pid()](#miscget_pid) + - [misc::get_uid()](#miscget_uid) + - [misc::generate_uuid()](#miscgenerate_uuid) +- [Operating System](#operating-system) + - [os::detect_os()](#osdetect_os) + - [os::detect_linux_distro()](#osdetect_linux_distro) + - [os::detect_linux_version()](#osdetect_linux_version) + - [os::detect_mac_version()](#osdetect_mac_version) +- [String](#string) + - [string::trim()](#stringtrim) + - [string::split()](#stringsplit) + - [string::lstrip()](#stringlstrip) + - [string::rstrip()](#stringrstrip) + - [string::to_lower()](#stringto_lower) + - [string::to_upper()](#stringto_upper) + - [string::contains()](#stringcontains) + - [string::starts_with()](#stringstarts_with) + - [string::ends_with()](#stringends_with) + - [string::regex()](#stringregex) +- [Terminal](#terminal) + - [terminal::is_term()](#terminalis_term) + - [terminal::detect_profile()](#terminaldetect_profile) + - [terminal::clear_line()](#terminalclear_line) +- [Validation](#validation) + - [validation::email()](#validationemail) + - [validation::ipv4()](#validationipv4) + - [validation::ipv6()](#validationipv6) + - [validation::alpha()](#validationalpha) + - [validation::alpha_num()](#validationalpha_num) + - [validation::alpha_dash()](#validationalpha_dash) + - [validation::version_comparison()](#validationversion_comparison) +- [Variable](#variable) + - [variable::is_array()](#variableis_array) + - [variable::is_numeric()](#variableis_numeric) + - [variable::is_int()](#variableis_int) + - [variable::is_float()](#variableis_float) + - [variable::is_bool()](#variableis_bool) + - [variable::is_true()](#variableis_true) + - [variable::is_false()](#variableis_false) + - [variable::is_empty_or_null()](#variableis_empty_or_null) +- [Inspired By](#inspired-by) +- [License](#license) + + +## Installation +The script can be installed and sourced using following methods. + +### Method 1 - Git Submodules +If the library is used inside a git project then git submodules can be used to install the library to the project. +Following command will initialize git submodule and download the library to `./vendor/bash-utility` folder. + +```shell +git submodule init +git submodule add -b master https://github.com/labbots/bash-utility vendor/bash-utility +``` + +To Update submodules to latest code execute the following command. + +```shell +git submodule update --rebase --remote +``` +Once the submodule is added or updated, make sure to commit changes to your repository. + +```shell +git add . +git commit -m 'Added/updated bash-utility library.' +``` +**Note:** When cloning your repository, use `--recurse-submodules` flag to `git clone` command to install the git sub modules. + +### Method 2 - Git Clone +If you don't want to use git submodules, you can use `git clone` to download library and then move the files to desired location manually. + +The below command will clone the repository to `vendor/bash-utility` folder in current working directory. + +```shell +git clone https://github.com/labbots/bash-utility.git ./vendor/bash-utility +``` +### Method 3 - Direct Download +If you do not have git installed, you can download the archive of the latest version of the library. Extract the zip file to appropriate folder by following the below command. + +```shell +wget https://github.com/labbots/bash-utility/archive/master.zip +unzip -q master.zip -d tmp +mkdir -p vendor/bash-utility +mv tmp/bash-utility-master vendor/bash-utility +rm tmp +``` + +## Usage +Bash utility functions can be used by simply sourcing the library script file to your own script. +To access all the functions within the bash-utility library, you could import the main bash file as follows. + +```shell +source "vendor/bash-utility/bash-utility.sh" +``` + +You can also only use the necessary library functions by only importing the required function files. + +```shell +source "vendor/bash-utility/src/array.sh" +``` + + + +## Array + +Functions for array operations and manipulations. + +### array::contains() + +Check if item exists in the given array. + +#### Arguments + +- **$1** (mixed): Item to search (needle). +- **$2** (array): array to be searched (haystack). + +#### Exit codes + +- **0**: If successful. +- **1**: If no match found in the array. +- **2**: Function missing arguments. + +#### Example + +```bash +array=("a" "b" "c") +array::contains "c" ${array[@]} +#Output +0 +``` + +### array::dedupe() + +Remove duplicate items from the array. + +#### Arguments + +- **$1** (array): Array to be deduped. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Deduplicated array. + +#### Example + +```bash +array=("a" "b" "a" "c") +printf "%s" "$(array::dedupe ${array[@]})" +#Output +a +b +c +``` + +### array::is_empty() + +Check if a given array is empty. + +#### Arguments + +- **$1** (array): Array to be checked. + +#### Exit codes + +- **0**: If the given array is empty. +- **2**: If the given array is not empty. + +#### Example + +```bash +array=("a" "b" "c" "d") +array::is_empty "${array[@]}" +``` + +### array::join() + +Join array elements with a string. + +#### Arguments + +- **$1** (string): String to join the array elements (glue). +- **$2** (array): array to be joined with glue string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- String containing a string representation of all the array elements in the same order,with the glue string between each element. + +#### Example + +```bash +array=("a" "b" "c" "d") +printf "%s" "$(array::join "," "${array[@]}")" +#Output +a,b,c,d +printf "%s" "$(array::join "" "${array[@]}")" +#Output +abcd +``` + +### array::reverse() + +Return an array with elements in reverse order. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- The reversed array. + +#### Example + +```bash +array=(1 2 3 4 5) +printf "%s" "$(array::reverse "${array[@]}")" +#Output +5 4 3 2 1 +``` + +### array::random_element() + +Returns a random item from the array. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Random item out of the array. + +#### Example + +```bash +array=("a" "b" "c" "d") +printf "%s\n" "$(array::random_element "${array[@]}")" +#Output +c +``` + +### array::sort() + +Sort an array from lowest to highest. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- sorted array. + +#### Example + +```bash +sarr=("a c" "a" "d" 2 1 "4 5") +array::array_sort "${sarr[@]}" +#Output +1 +2 +4 5 +a +a c +d +``` + +### array::rsort() + +Sort an array in reverse order (highest to lowest). + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- reverse sorted array. + +#### Example + +```bash +sarr=("a c" "a" "d" 2 1 "4 5") +array::array_sort "${sarr[@]}" +#Output +d +a c +a +4 5 +2 +1 +``` + +### array::bsort() + +Bubble sort an integer array from lowest to highest. +This sort does not work on string array. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- bubble sorted array. + +#### Example + +```bash +iarr=(4 5 1 3) +array::bsort "${iarr[@]}" +#Output +1 +3 +4 +5 +``` + +### array::merge() + +Merge two arrays. +Pass the variable name of the array instead of value of the variable. + +#### Arguments + +- **$1** (string): variable name of first array. +- **$2** (string): variable name of second array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Merged array. + +#### Example + +```bash +a=("a" "c") +b=("d" "c") +array::merge "a[@]" "b[@]" +#Output +a +c +d +c +``` + +## Check + +Helper functions. + +### check::command_exists() + +Check if the command exists in the system. + +#### Arguments + +- **$1** (string): Command name to be searched. + +#### Exit codes + +- **0**: If the command exists. +- **1**: If the command does not exist. +- **2**: Function missing arguments. + +#### Example + +```bash +check::command_exists "tput" +``` + +### check::is_sudo() + +Check if the script is executed with sudo privilege. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If the script is executed with root privilege. +- **1**: If the script is not executed with root privilege + +#### Example + +```bash +check::is_sudo +``` + +## Collection + +(Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. + +### collection::each() + +Iterates over elements of collection and invokes iteratee for each element. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output of iteratee function. + +#### Example + +```bash +test_func(){ + printf "print value: %s\n" "$1" + return 0 + } +arr1=("a b" "c d" "a" "d") +printf "%s\n" "${arr1[@]}" | collection::each "test_func" +collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach +#output + print value: a b + print value: c d + print value: a + print value: d +``` + +#### Example + +```bash +# If other function from this library is already used to process the array. +# Then following method could be used to pass the array to the function. +out=("$(array::dedupe "${arr1[@]}")") +collection::each "test_func" <<< "${out[@]}" +``` + +### collection::every() + +Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **1**: If iteratee function fails. +- **2**: Function missing arguments. + +#### Example + +```bash +arri=("1" "2" "3" "4") +printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" +``` + +### collection::filter() + +Iterates over elements of array, returning all elements where iteratee returns true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- array values matching the iteratee function. + +#### Example + +```bash +arri=("1" "2" "3" "a") +printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" +#output +1 +2 +3 +``` + +### collection::find() + +Iterates over elements of collection, returning the first element where iteratee returns true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Output on stdout + +- first array value matching the iteratee function. + +#### Example + +```bash +arr=("1" "2" "3" "a") +check_a(){ + [[ "$1" = "a" ]] +} +printf "%s\n" "${arr[@]}" | collection::find "check_a" +#Output +a +``` + +### collection::invoke() + +Invokes the iteratee with each element passed as argument to the iteratee. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output from the iteratee function. + +#### Example + +```bash +opt=("-a" "-l") +printf "%s\n" "${opt[@]}" | collection::invoke "ls" +``` + +### collection::map() + +Creates an array of values by running each element in array through iteratee. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output result of iteratee on value. + +#### Example + +```bash +arri=("1" "2" "3") +add_one(){ + i=${1} + i=$(( i + 1 )) + printf "%s\n" "$i" +} +printf "%s\n" "${arri[@]}" | collection::map "add_one" +``` + +### collection::reject() + +The opposite of filter function; this method returns the elements of collection that iteratee does not return true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- array values not matching the iteratee function. + +#### Example + +```bash +arri=("1" "2" "3" "a") +printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" +#Ouput +a +``` + +#### See also + +- [collection::filter](#collectionfilter) + +### collection::some() + +Checks if iteratee returns true for any element of the array. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If match successful. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +arr=("a" "b" "3" "a") +printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" +``` + +## Date + +Functions for manipulating dates. + +### date::now() + +Get current time in unix timestamp. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- current timestamp. + +#### Example + +```bash +echo "$(date::now)" +#Output +1591554426 +``` + +### date::epoc() + +convert datetime string to unix timestamp. + +#### Arguments + +- **$1** (string): date time in any format. + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp for specified datetime. + +#### Example + +```bash +echo "$(date::epoc "2020-07-07 18:38")" +#Output +1594143480 +``` + +### date::add_days_from() + +Add number of days from specified timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_days_from "1594143480")" +#Output +1594229880 +``` + +### date::add_months_from() + +Add number of months from specified timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_months_from "1594143480")" +#Output +1596821880 +``` + +### date::add_years_from() + +Add number of years from specified timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_years_from "1594143480")" +#Output +1625679480 +``` + +### date::add_weeks_from() + +Add number of weeks from specified timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_weeks_from "1594143480")" +#Output +1594748280 +``` + +### date::add_hours_from() + +Add number of hours from specified timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_hours_from "1594143480")" +#Output +1594147080 +``` + +### date::add_minutes_from() + +Add number of minutes from specified timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_minutes_from "1594143480")" +#Output +1594143540 +``` + +### date::add_seconds_from() + +Add number of seconds from specified timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_seconds_from "1594143480")" +#Output +1594143481 +``` + +### date::add_days() + +Add number of days from current day timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_days "1")" +#Output +1591640826 +``` + +### date::add_months() + +Add number of months from current day timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_months "1")" +#Output +1594146426 +``` + +### date::add_years() + +Add number of years from current day timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_years "1")" +#Output +1623090426 +``` + +### date::add_weeks() + +Add number of weeks from current day timestamp. +If number of weeks not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_weeks "1")" +#Output +1592159226 +``` + +### date::add_hours() + +Add number of hours from current day timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_hours "1")" +#Output +1591558026 +``` + +### date::add_minutes() + +Add number of minutes from current day timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_minutes "1")" +#Output +1591554486 +``` + +### date::add_seconds() + +Add number of seconds from current day timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_seconds "1")" +#Output +1591554427 +``` + +### date::sub_days_from() + +Subtract number of days from specified timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_days_from "1594143480")" +#Output +1594057080 +``` + +### date::sub_months_from() + +Subtract number of months from specified timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_months_from "1594143480")" +#Output +1591551480 +``` + +### date::sub_years_from() + +Subtract number of years from specified timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_years_from "1594143480")" +#Output +1562521080 +``` + +### date::sub_weeks_from() + +Subtract number of weeks from specified timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_weeks_from "1594143480")" +#Output +1593538680 +``` + +### date::sub_hours_from() + +Subtract number of hours from specified timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_hours_from "1594143480")" +#Output +1594139880 +``` + +### date::sub_minutes_from() + +Subtract number of minutes from specified timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_minutes_from "1594143480")" +#Output +1594143420 +``` + +### date::sub_seconds_from() + +Subtract number of seconds from specified timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_seconds_from "1594143480")" +#Output +1594143479 +``` + +### date::sub_days() + +Subtract number of days from current day timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_days "1")" +#Output +1588876026 +``` + +### date::sub_months() + +Subtract number of months from current day timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_months "1")" +#Output +1559932026 +``` + +### date::sub_years() + +Subtract number of years from current day timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_years "1")" +#Output +1591468026 +``` + +### date::sub_weeks() + +Subtract number of weeks from current day timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_weeks "1")" +#Output +1590949626 +``` + +### date::sub_hours() + +Subtract number of hours from current day timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_hours "1")" +#Output +1591550826 +``` + +### date::sub_minutes() + +Subtract number of minutes from current day timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_minutes "1")" +#Output +1591554366 +``` + +### date::sub_seconds() + +Subtract number of seconds from current day timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_seconds "1")" +#Output +1591554425 +``` + +### date::format() + +Format unix timestamp to human readable format. +If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (string): format control characters based on `date` command (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate time string. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted time string. + +#### Example + +```bash +echo echo "$(date::format "1594143480")" +#Output +2020-07-07 18:38:00 +``` + +## Debug + +Functions to facilitate debugging scripts. + +### debug::print_array() + +Prints the content of array as key value pair for easier debugging. +Pass the variable name of the array instead of value of the variable. + +#### Arguments + +- **$1** (string): variable name of the array. + +#### Output on stdout + +- Formatted key value of array. + +#### Example + +```bash +array=(foo bar baz) +printf "Array\n" +printarr "array" +declare -A assoc_array +assoc_array=([foo]=bar [baz]=foobar) +printf "Assoc Array\n" +printarr "assoc_array" +#Output +Array +0 = foo +1 = bar +2 = baz +Assoc Array +baz = foobar +foo = bar +``` + +### debug::print_ansi() + +Function to print ansi escape sequence as is. +This function helps debug ansi escape sequence in text by displaying the escape codes. + +#### Arguments + +- **$1** (string): input with ansi escape sequence. + +#### Output on stdout + +- Ansi escape sequence printed in output as is. + +#### Example + +```bash +txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" +debug::print_ansi "$txt" +#Output +\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m +``` + +## File + +Functions for handling files. + +### file::make_temp_file() + +Create temporary file. +Function creates temporary file with random name. The temporary file will be deleted when script finishes. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If successful. +- **1**: If failed to create temp file. + +#### Output on stdout + +- file name of temporary file created. + +#### Example + +```bash +echo "$(file::make_temp_file)" +#Output +tmp.vgftzy +``` + +### file::make_temp_dir() + +Create temporary directory. +Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. + +#### Arguments + +- **$1** (string): Temporary directory prefix +- $2 string Flag to auto remove directory on exit trap (true) + +#### Exit codes + +- **0**: If successful. +- **1**: If failed to create temp directory. +- **2**: Missing arguments. + +#### Output on stdout + +- directory name of temporary directory created. + +#### Example + +```bash +echo "$(utility::make_temp_dir)" +#Output +tmp.rtfsxy +``` + +### file::name() + +Get only the filename from string path. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- name of the file with extension. + +#### Example + +```bash +echo "$(file::name "/path/to/test.md")" +#Output +test.md +``` + +### file::basename() + +Get the basename of file from file name. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- basename of the file. + +#### Example + +```bash +echo "$(file::basename "/path/to/test.md")" +#Output +test +``` + +### file::extension() + +Get the extension of file from file name. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **1**: If no extension is found in the filename. +- **2**: Function missing arguments. + +#### Output on stdout + +- extension of the file. + +#### Example + +```bash +echo "$(file::extension "/path/to/test.md")" +#Output +md +``` + +### file::dirname() + +Get directory name from file path. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- directory path. + +#### Example + +```bash +echo "$(file::dirname "/path/to/test.md")" +#Output +/path/to +``` + +### file::full_path() + +Get absolute path of file or directory. + +#### Arguments + +- **$1** (string): relative or absolute path to file/direcotry. + +#### Exit codes + +- **0**: If successful. +- **1**: If file/directory does not exist. +- **2**: Function missing arguments. + +#### Output on stdout + +- Absolute path to file/directory. + +#### Example + +```bash +file::full_path "../path/to/file.md" +#Output +/home/labbots/docs/path/to/file.md +``` + +### file::mime_type() + +Get mime type of provided input. + +#### Arguments + +- **$1** (string): relative or absolute path to file/directory. + +#### Exit codes + +- **0**: If successful. +- **1**: If file/directory does not exist. +- **2**: Function missing arguments. +- **3**: If file or mimetype command not found in system. + +#### Output on stdout + +- mime type of file/directory. + +#### Example + +```bash +file::mime_type "../src/file.sh" +#Output +application/x-shellscript +``` + +### file::contains_text() + +Search if a given pattern is found in file. + +#### Arguments + +- **$1** (string): relative or absolute path to file/directory. +- **$2** (string): search key or regular expression. + +#### Exit codes + +- **0**: If given search parameter is found in file. +- **1**: If search paramter not found in file. +- **2**: Function missing arguments. + +#### Example + +```bash +file::contains_text "./file.sh" "^[ @[:alpha:]]*" +file::contains_text "./file.sh" "@file" +#Output +0 +``` + +## Format + +Functions to format provided input. + +### format::human_readable_seconds() + +Format seconds to human readable format. + +#### Arguments + +- **$1** (int): number of seconds. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted time string. + +#### Example + +```bash +echo "$(format::human_readable_seconds "356786")" +#Output +4 days 3 hours 6 minute(s) and 26 seconds +``` + +### format::bytes_to_human() + +Format bytes to human readable format. + +#### Arguments + +- **$1** (int): size in bytes. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted file size string. + +#### Example + +```bash +echo "$(format::bytes_to_human "2250")" +#Output +2.19 KB +``` + +### format::strip_ansi() + +Remove Ansi escape sequences from given text. + +#### Arguments + +- **$1** (string): Input text to be ansi stripped. + +#### Exit codes + +- **0**: If successful. + +#### Output on stdout + +- Ansi stripped text. + +#### Example + +```bash +format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" +#Output +This is bold red text.This is green text. +``` + +### format::text_center() + +Prints the given text to centre of terminal. + +#### Arguments + +- **$1** (string): Text to be printed. +- **$2** (string): Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted text. + +#### Example + +```bash +format::text_center "This text is in centre of the terminal." "-" +``` + +### format::report() + +Format String to print beautiful report. + +#### Arguments + +- **$1** (string): Text to be printed on the left. +- **$2** (string): Text to be printed within the square brackets. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted text. + +#### Example + +```bash +format::report "Initialising mission state" "Success" +#Output +Initialising mission state ....................................................................[ Success ] +``` + +### format::trim_text_to_term() + +Trim given text to width of the terminal window. + +#### Arguments + +- **$1** (string): Text of first sentence. +- **$2** (string): Text of second sentence (optional). + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- trimmed text. + +#### Example + +```bash +format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." +#Output +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. +``` + +## Interaction + +Functions to enable interaction with the user. + +### interaction::prompt_yes_no() + +Prompt yes or no question to the user. + +#### Arguments + +- **$1** (string): The question to be prompted to the user. +- **$2** (string): default answer \[yes/no\] (optional). + +#### Exit codes + +- **0**: If user responds with yes. +- **1**: If user responds with no. +- **2**: Function missing arguments. + +#### Example + +```bash +interaction::prompt_yes_no "Are you sure to proceed" "yes" +#Output +Are you sure to proceed (y/n)? [y] +``` + +### interaction::prompt_response() + +Prompt question to the user. + +#### Arguments + +- **$1** (string): The question to be prompted to the user. +- **$2** (string): default answer (optional). + +#### Exit codes + +- **0**: If user responds with answer. +- **2**: Function missing arguments. + +#### Output on stdout + +- User entered answer to the question. + +#### Example + +```bash +interaction::prompt_response "Choose directory to install" "/home/path" +#Output +Choose directory to install? [/home/path] +``` + +## Json + +Simple json manipulation. These functions does not completely replace `jq` in any way. + +### json::get_value() + +Extract value from json based on key and position. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): id of the field to fetch. +- **$2** (int): position of value to extract.Defaults to 1.(optional) + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- string value of extracted key. + +#### Example + +```bash +json::get_value "id" "1" < json_file +json::get_value "id" <<< "${json_var}" +echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" +``` + +## Miscellaneous + +Set of miscellaneous helper functions. + +### misc::check_internet_connection() + +Check if internet connection is available. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script can connect to internet. +- **1**: If script cannot access internet. + +#### Example + +```bash +misc::check_internet_connection +``` + +### misc::get_pid() + +Get list of process ids based on process name. + +#### Arguments + +- **$1** (Name): of the process to search. + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- list of process ids. + +#### Example + +```bash +misc::get_pid "chrome" +#Ouput +25951 +26043 +26528 +26561 +``` + +### misc::get_uid() + +Get user id based on username. + +#### Arguments + +- **$1** (username): to search. + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- string uid for the username. + +#### Example + +```bash +misc::get_uid "labbots" +#Ouput +1000 +``` + +### misc::generate_uuid() + +Generate random uuid. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If match successful. + +#### Output on stdout + +- random generated uuid. + +#### Example + +```bash +misc::generate_uuid +#Ouput +65bc64d1-d355-4ffc-a9d9-dc4f3954c34c +``` + +## Operating System + +Functions to detect Operating system and version. + +### os::detect_os() + +Identify the OS the function is run on. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If OS is successfully detected. +- **1**: If unable to detect OS. + +#### Output on stdout + +- Operating system name (linux, mac or windows). + +#### Example + +```bash +os::detect_os +#Output +linux +``` + +### os::detect_linux_distro() + +Identify the distribution flavour of linux. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If Linux distro is successfully detected. +- **1**: If unable to detect OS distro. + +#### Output on stdout + +- Linux OS distribution name (ubuntu, debian, suse, etc.,). + +#### Example + +```bash +os::detect_linux_distro +#Output +ubuntu +``` + +### os::detect_linux_version() + +Identify the Linux version. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If Linux version is successfully detected. +- **1**: If unable to detect Linux version. + +#### Output on stdout + +- Linux OS version number (18.04, 20.04, etc.,). + +#### Example + +```bash +os::detect_linux_version +#Output +20.04 +``` + +### os::detect_mac_version() + +Identify the MacOS version. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If MacOS version is successfully detected. +- **1**: If unable to detect MacOS version. + +#### Output on stdout + +- MacOS version number (10.15.6, etc.,) + +#### Example + +```bash +os::detect_linux_version +#Output +10.15.7 +``` + +## String + +Functions for string operations and manipulations. + +### string::trim() + +Strip whitespace from the beginning and end of a string. + +#### Arguments + +- **$1** (string): The string to be trimmed. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- The trimmed string. + +#### Example + +```bash +echo "$(string::trim " Hello World! ")" +#Output +Hello World! +``` + +### string::split() + +Split a string to array by a delimiter. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The delimiter string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns an array of strings created by splitting the string parameter by the delimiter. + +#### Example + +```bash +array=( $(string::split "a,b,c" ",") ) +printf "%s" "$(string::split "Hello!World" "!")" +#Output +Hello +World +``` + +### string::lstrip() + +Strip characters from the beginning of a string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The characters you want to strip. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the modified string. + +#### Example + +```bash +echo "$(string::lstrip "Hello World!" "He")" +#Output +llo World! +``` + +### string::rstrip() + +Strip characters from the end of a string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The characters you want to strip. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the modified string. + +#### Example + +```bash +echo "$(string::rstrip "Hello World!" "d!")" +#Output +Hello Worl +``` + +### string::to_lower() + +Make a string lowercase. + +#### Arguments + +- **$1** (string): The input string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the lowercased string. + +#### Example + +```bash +echo "$(string::to_lower "HellO")" +#Output +hello +``` + +### string::to_upper() + +Make a string all uppercase. + +#### Arguments + +- **$1** (string): The input string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the uppercased string. + +#### Example + +```bash +echo "$(string::to_upper "HellO")" +#Output +HELLO +``` + +### string::contains() + +Check whether the search string exists within the input string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::contains "Hello World!" "lo" +``` + +### string::starts_with() + +Check whether the input string starts with key string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::starts_with "Hello World!" "He" +``` + +### string::ends_with() + +Check whether the input string ends with key string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::ends_with "Hello World!" "d!" +``` + +### string::regex() + +Check whether the input string matches the given regex. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::regex "HELLO" "^[A-Z]*$" +``` + +## Terminal + +Set of useful terminal functions. + +### terminal::is_term() + +Check if script is run in terminal. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script is run on terminal. +- **1**: If script is not run on terminal. + +### terminal::detect_profile() + +Detect profile rc file for zsh and bash of current script running user. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script is run on terminal. +- **1**: If script is not run on terminal. + +#### Output on stdout + +- path to the profile file. + +### terminal::clear_line() + +Clear the output in terminal on the specified line number. +This function clears line only on terminal. + +#### Arguments + +- **$1** (Line): number to clear. Defaults to 1. (optional) + +#### Exit codes + +- **0**: If script is run on terminal. + +#### Output on stdout + +- clear line ansi code. + +## Validation + +Functions to perform validation on given data. + +### validation::email() + +Validate whether a given input is a valid email address or not. + +#### Arguments + +- **$1** (string): input email address to validate. + +#### Exit codes + +- **0**: If provided input is an email address. +- **1**: If provided input is not an email address. +- **2**: Function missing arguments. + +#### Example + +```bash +test='test@gmail.com' +validation::email "${test}" +echo $? +#Output +0 +``` + +### validation::ipv4() + +Validate whether a given input is a valid IP V4 address. + +#### Arguments + +- **$1** (string): input IPv4 address. + +#### Exit codes + +- **0**: If provided input is a valid IPv4. +- **1**: If provided input is not a valid IPv4. +- **2**: Function missing arguments. + +#### Example + +```bash +ips=' + 4.2.2.2 + a.b.c.d + 192.168.1.1 + 0.0.0.0 + 255.255.255.255 + 255.255.255.256 + 192.168.0.1 + 192.168.0 + 1234.123.123.123 + 0.192.168.1 + ' +for ip in $ips; do + if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi + printf "%-20s: %s\n" "$ip" "$stat" +done +#Output +4.2.2.2 : good +a.b.c.d : bad +192.168.1.1 : good +0.0.0.0 : good +255.255.255.255 : good +255.255.255.256 : bad +192.168.0.1 : good +192.168.0 : bad +1234.123.123.123 : bad +0.192.168.1 : good +``` + +### validation::ipv6() + +Validate whether a given input is a valid IP V6 address. + +#### Arguments + +- **$1** (string): input IPv6 address. + +#### Exit codes + +- **0**: If provided input is a valid IPv6. +- **1**: If provided input is not a valid IPv6. +- **2**: Function missing arguments. + +#### Example + +```bash +ips=' + 2001:db8:85a3:8d3:1319:8a2e:370:7348 + fe80::1ff:fe23:4567:890a + fe80::1ff:fe23:4567:890a%eth2 + 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar + fezy::1ff:fe23:4567:890a + :: + 2001:db8:: + ' +for ip in $ips; do + if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi + printf "%-50s= %s\n" "$ip" "$stat" +done +#Output +2001:db8:85a3:8d3:1319:8a2e:370:7348 = good +fe80::1ff:fe23:4567:890a = good +fe80::1ff:fe23:4567:890a%eth2 = good +2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad +fezy::1ff:fe23:4567:890a = bad +:: = good +2001:db8:: = good +``` + +### validation::alpha() + +Validate if given variable is entirely alphabetic characters. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is only alpha characters. +- **1**: If input contains any non alpha characters. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abcABC' +validation::alpha "${test}" +echo $? +#Output +0 +``` + +### validation::alpha_num() + +Check if given variable contains only alpha-numeric characters. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is an alpha-numeric. +- **1**: If input is not an alpha-numeric. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abc123' +validation::alpha_num "${test}" +echo $? +#Output +0 +``` + +### validation::alpha_dash() + +Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is valid. +- **1**: If input the input is not valid. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abc-ABC_cD' +validation::alpha_dash "${test}" +echo $? +#Output +0 +``` + +### validation::version_comparison() + +Compares version numbers and provides return based on whether the value in equal, less than or greater. + +#### Arguments + +- **$1** (string): Version number to check (eg: 1.0.1) + +#### Exit codes + +- **0**: version number is equal. +- **1**: $1 version number is greater than $2. +- **2**: $1 version number is less than $2. +- **3**: Function is missing required arguments. +- **4**: Provided input argument is in invalid format. + +#### Example + +```bash +test='abc-ABC_cD' +validation::version_comparison "12.0.1" "12.0.1" +echo $? +#Output +0 +``` + +## Variable + +Functions for handling variables. + +### variable::is_array() + +Check if given variable is array. +Pass the variable name instead of value of the variable. + +#### Arguments + +- **$1** (string): name of the variable to check. + +#### Exit codes + +- **0**: If input is array. +- **1**: If input is not an array. + +#### Example + +```bash +arr=("a" "b" "c") +variable::is_array "arr" +#Output +0 +``` + +### variable::is_numeric() + +Check if given variable is a number. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is number. +- **1**: If input is not a number. + +#### Example + +```bash +variable::is_numeric "1234" +#Output +0 +``` + +### variable::is_int() + +Check if given variable is an integer. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is an integer. +- **1**: If input is not an integer. + +#### Example + +```bash +variable::is_int "+1234" +#Output +0 +``` + +### variable::is_float() + +Check if given variable is a float. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is a float. +- **1**: If input is not a float. + +#### Example + +```bash +variable::is_float "+1234.0" +#Output +0 +``` + +### variable::is_bool() + +Check if given variable is a boolean. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is a boolean. +- **1**: If input is not a boolean. + +#### Example + +```bash +variable::is_bool "true" +#Output +0 +``` + +### variable::is_true() + +Check if given variable is a true. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is true. +- **1**: If input is not true. + +#### Example + +```bash +variable::is_true "true" +#Output +0 +``` + +### variable::is_false() + +Check if given variable is false. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is false. +- **1**: If input is not false. + +#### Example + +```bash +variable::is_false "false" +#Output +0 +``` + +### variable::is_empty_or_null() + +Check if given variable is empty or null. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is empty or null. +- **1**: If input is not empty or null. + +#### Example + +```bash +test='' +variable::is_empty_or_null $test +#Output +0 +``` + + + +## Inspired By + +- [Bash Bible](https://github.com/dylanaraps/pure-bash-bible) - A collection of pure bash alternatives to external processes. + +## License + +[MIT](https://github.com/labbots/google-drive-upload/blob/master/LICENSE) diff --git a/src/configng/functions/bash-utility-master/bash_utility.sh b/src/configng/functions/bash-utility-master/bash_utility.sh new file mode 100644 index 000000000..65411add0 --- /dev/null +++ b/src/configng/functions/bash-utility-master/bash_utility.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +source src/array.sh +source src/string.sh +source src/variable.sh +source src/file.sh +source src/misc.sh +source src/date.sh +source src/interaction.sh +source src/check.sh +source src/format.sh +source src/collection.sh +source src/json.sh +source src/terminal.sh +source src/validation.sh +source src/debug.sh +source src/os.sh + + diff --git a/src/configng/functions/bash-utility-master/bin/bashdoc.awk b/src/configng/functions/bash-utility-master/bin/bashdoc.awk new file mode 100644 index 000000000..13efdaf7b --- /dev/null +++ b/src/configng/functions/bash-utility-master/bin/bashdoc.awk @@ -0,0 +1,275 @@ +#!/usr/bin/awk -f + +# Varibles +# style = readme or doc +# toc = true or false +BEGIN { + if (! style) { + style = "doc" + } + + if (! toc) { + toc = 0 + } + + styles["empty", "from"] = ".*" + styles["empty", "to"] = "" + + styles["h1", "from"] = ".*" + styles["h1", "to"] = "# &" + + styles["h2", "from"] = ".*" + styles["h2", "to"] = "## &" + + styles["h3", "from"] = ".*" + styles["h3", "to"] = "### &" + + styles["h4", "from"] = ".*" + styles["h4", "to"] = "#### &" + + styles["h5", "from"] = ".*" + styles["h5", "to"] = "##### &" + + styles["code", "from"] = ".*" + styles["code", "to"] = "```&" + + styles["/code", "to"] = "```" + + styles["argN", "from"] = "^(\\$[0-9]) (\\S+)" + styles["argN", "to"] = "**\\1** (\\2):" + + styles["arg@", "from"] = "^\\$@ (\\S+)" + styles["arg@", "to"] = "**...** (\\1):" + + styles["li", "from"] = ".*" + styles["li", "to"] = "- &" + + styles["i", "from"] = ".*" + styles["i", "to"] = "*&*" + + styles["anchor", "from"] = ".*" + styles["anchor", "to"] = "[&](#&)" + + styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" + styles["exitcode", "to"] = "**\\1**: \\2" + + styles["h_rule", "to"] = "---" + + styles["comment", "from"] = ".*" + styles["comment", "to"] = "" + + + output_format["readme", "h1"] = "h2" + output_format["readme", "h2"] = "h3" + output_format["readme", "h3"] = "h4" + output_format["readme", "h4"] = "h5" + + output_format["bashdoc", "h1"] = "h1" + output_format["bashdoc", "h2"] = "h2" + output_format["bashdoc", "h3"] = "h3" + output_format["bashdoc", "h4"] = "h4" + + output_format["webdoc", "h1"] = "empty" + output_format["webdoc", "h2"] = "h3" + output_format["webdoc", "h3"] = "h4" + output_format["webdoc", "h4"] = "h5" + +} + +function render(type, text) { + if((style,type) in output_format){ + type = output_format[style,type] + } + return gensub( \ + styles[type, "from"], + styles[type, "to"], + "g", + text \ + ) +} + +function render_list(item, anchor) { + return "- [" item "](#" anchor ")" +} + +function generate_anchor(text) { + # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 + text = tolower(text) + gsub(/[^[:alnum:]_ -]/, "", text) + gsub(/ /, "-", text) + return text +} + +function reset() { + has_example = 0 + has_args = 0 + has_exitcode = 0 + has_stdout = 0 + + content_desc = "" + content_example = "" + content_args = "" + content_exitcode = "" + content_seealso = "" + content_stdout = "" +} + +/^[[:space:]]*# @internal/ { + is_internal = 1 +} + +/^[[:space:]]*# @file/ { + sub(/^[[:space:]]*# @file /, "") + + filedoc = render("h1", $0) "\n" + if(style == "webdoc"){ + filedoc = filedoc render("comment", "file=" $0) "\n" + } + +} + +/^[[:space:]]*# @brief/ { + sub(/^[[:space:]]*# @brief /, "") + if(style == "webdoc"){ + filedoc = filedoc render("comment", "brief=" $0) "\n" + } + filedoc = filedoc "\n" $0 +} + +/^[[:space:]]*# @description/ { + in_description = 1 + in_example = 0 + + reset() + + docblock = "" +} + +in_description { + if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) { + if (!match(content_desc, /\n$/)) { + content_desc = content_desc "\n" + } + in_description = 0 + } else { + sub(/^[[:space:]]*# @description /, "") + sub(/^[[:space:]]*# /, "") + sub(/^[[:space:]]*#$/, "") + + content_desc = content_desc "\n" $0 + } +} + +in_example { + + if (! /^[[:space:]]*#[ ]{3}/) { + + in_example = 0 + + content_example = content_example "\n" render("/code") "\n" + } else { + sub(/^[[:space:]]*#[ ]{3}/, "") + + content_example = content_example "\n" $0 + } +} + +/^[[:space:]]*# @example/ { + in_example = 1 + content_example = content_example "\n" render("h3", "Example") + content_example = content_example "\n\n" render("code", "bash") +} + +/^[[:space:]]*# @arg/ { + if (!has_args) { + has_args = 1 + + content_args = content_args "\n" render("h3", "Arguments") "\n\n" + } + + sub(/^[[:space:]]*# @arg /, "") + + $0 = render("argN", $0) + $0 = render("arg@", $0) + + content_args = content_args render("li", $0) "\n" +} + +/^[[:space:]]*# @noargs/ { + content_args = content_args "\n" render("i", "Function has no arguments.") "\n" +} + +/^[[:space:]]*# @exitcode/ { + if (!has_exitcode) { + has_exitcode = 1 + + content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n" + } + + sub(/^[[:space:]]*# @exitcode /, "") + + $0 = render("exitcode", $0) + + content_exitcode = content_exitcode render("li", $0) "\n" +} + +/^[[:space:]]*# @see/ { + sub(/[[:space:]]*# @see /, "") + anchor = generate_anchor($0) + $0 = render_list($0, anchor) + + content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n" +} + +/^[[:space:]]*# @stdout/ { + has_stdout = 1 + + sub(/^[[:space:]]*# @stdout /, "") + + content_stdout = content_stdout "\n" render("h3", "Output on stdout") + content_stdout = content_stdout "\n\n" render("li", $0) "\n" +} + +{ + docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso + if(style == "webdoc"){ + docblock = docblock "\n" render("h_rule") "\n" + } +} + +/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example { + if (is_internal) { + is_internal = 0 + } else { + func_name = gensub(\ + /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \ + "\\3()", \ + "g" \ + ) + doc = doc "\n" render("h2", func_name) "\n" docblock + if (toc) { + url = generate_anchor(func_name) + + content_idx = content_idx "\n" "- [" func_name "](#" url ")" + } + } + + docblock = "" + reset() +} + +END { + if (filedoc != "") { + print filedoc + } + + if (toc) { + print "" + print render("h2", "Table of Contents") + print content_idx + print "" + print render("h_rule") + } + + print doc +} diff --git a/src/configng/functions/bash-utility-master/bin/generate_readme.sh b/src/configng/functions/bash-utility-master/bin/generate_readme.sh new file mode 100644 index 000000000..858a4b8be --- /dev/null +++ b/src/configng/functions/bash-utility-master/bin/generate_readme.sh @@ -0,0 +1,400 @@ +#!/usr/bin/env bash + +#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh +#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh +_usage() { + printf " +Script to autogenerate markdown based on bash source code.\n +The script generates table of contents and bashdoc and update the given markdown file.\n +Usage:\n %s [options.. ]\n +Options:\n + -f | --file - Relative or absolute path to the README.md file. + -s | --sh-dir - path to the bash script source folder to generate shdocs.\n + -l | --toc-level - Minimum level of header to print in Table of Contents.\n + -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n + -w | --webdoc - Flag to indicate generation of webdoc.\n + -p | --dest-dir - Path in which wedoc files must be generated.\n + -h | --help - Display usage instructions.\n" "${0##*/}" + exit 0 +} + +_setup_arguments() { + + unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR + MINLEVEL=1 + MAXLEVEL=3 + SCRIPT_FILE="${0##*/}" + declare source="${BASH_SOURCE[0]}" + while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink + SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" + source="$(readlink "$source")" + [[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located + done + SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" + SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md" + SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src" + WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions" + + SHORTOPTS="whp:f:m:d:s:-:" + + while getopts "${SHORTOPTS}" OPTION; do + case "${OPTION}" in + -) + _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } + case "${OPTARG}" in + help) + _usage + ;; + file) + _check_longoptions "${!OPTIND}" + SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-level) + _check_longoptions "${!OPTIND}" + MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-depth) + _check_longoptions "${!OPTIND}" + MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + sh-dir) + _check_longoptions "${!OPTIND}" + SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + webdoc) + WEBDOC=true + ;; + dest-dir) + WEBDOC_DEST_DIR="${OPTARG}" + ;; + '') + _usage + ;; + *) + printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + ;; + h) + _usage + ;; + f) + SOURCE_MARKDOWN="${OPTARG}" + ;; + m) + MINLEVEL="${OPTARG}" + ;; + d) + MAXLEVEL="${OPTARG}" + ;; + s) + SOURCE_SCRIPT_DIR="${OPTARG}" + ;; + w) + WEBDOC=true + ;; + p) + WEBDOC_DEST_DIR="${OPTARG}" + ;; + :) + printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + ?) + printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + done + shift "$((OPTIND - 1))" + + if [[ -w "${SOURCE_MARKDOWN}" ]]; then + declare src_file src_extension + src_file="${SOURCE_MARKDOWN##*/}" + src_extension="${src_file##*.}" + if [[ "${src_extension,,}" != "md" ]]; then + printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 + fi + else + printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 + fi + + if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then + printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 + fi + + declare re='^[0-9]+$' + if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then + echo "error: Not a number" >&2 + exit 1 + fi + if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then + printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 + fi + + [ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}" + +} + +_setup_tempfile() { + declare temp_file + type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } + trap 'rm -f "${temp_file}"' EXIT + printf "%s" "${temp_file}" +} + +_generate_shdoc() { + declare file + file="$(realpath "${1}")" + if [[ -s "${file}" ]]; then + awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2" + #awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md" + fi +} + +_insert_shdoc_to_file() { + declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc + shdoc_tmp_file="$1" + source_markdown="$2" + + start_shdoc="" + info_shdoc="" + end_shdoc="" + + sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" + + if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then + # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file + + sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" + echo -e "Updated bashdoc content to ${source_markdown} successfully\n" + + else + { + printf "%s\n" "${start_shdoc}" + cat "${shdoc_tmp_file}" + printf "%s\n" "${end_shdoc}" + } >> "${source_markdown}" + echo -e "Created bashdoc content to ${source_markdown} successfully\n" + fi +} + +_process_sh_files() { + declare shdoc_tmp_file source_script_dir source_markdown + source_markdown="${1}" + source_script_dir="${2}" + shdoc_tmp_file=$(_setup_tempfile) + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + while IFS= read -r -d '' line; do + _generate_shdoc "${line}" "${shdoc_tmp_file}" + done + _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" + rm "${shdoc_tmp_file}" + +} + +_generate_toc() { + + declare line level title anchor output counter temp_output invalid_chars + + invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" + while IFS='' read -r line || [[ -n "${line}" ]]; do + level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" + title="$(echo "${line}" | sed -E 's/^#+ //')" + [[ "${title}" = "Table of Contents" ]] && continue + + # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text + anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" + + # check new line introduced is not duplicated, if is duplicated, introduce a number at the end + temp_output=$output"$level- [$title](#$anchor)\n" + counter=1 + while true; do + nlines="$(echo -e "${temp_output}" | wc -l)" + duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)" + if [ "${nlines}" = "${duplines}" ]; then + break + fi + temp_output=$output"$level- [$title](#$anchor-$counter)\n" + counter=$((counter + 1)) + done + + output="$temp_output" + + # grep: filter header candidates to be included in toc + # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment + done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")" + + # when in toc we have two `--` quit one + echo "$output" + +} + +_insert_toc_to_file() { + + declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash + source_markdown="${1}" + toc_text="${2}" + start_toc="" + info_toc="" + end_toc="" + + toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" + # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command + utext_ampersand="id8234923000230gzz" + utext_slash="id9992384923423gzz" + toc_block="${toc_block//\&/${utext_ampersand}}" + toc_block="${toc_block//\//${utext_slash}}" + + # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 + # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches + if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then + # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace + sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" + echo -e "Updated TOC content in ${source_markdown} succesfully\n" + + else + sed -i 1i"$toc_block" "${source_markdown}" + echo -e "Created TOC in ${source_markdown} succesfully\n" + + fi + + # undo symbol replacements + sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" + sed -i "s,${utext_slash},\/,g" "${source_markdown}" + +} + +_process_toc() { + declare toc_temp_file source_markdown level toc_text + source_markdown="${1}" + + toc_temp_file=$(_setup_tempfile) + + sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" + + level=$MINLEVEL + while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do + level=$((level + 1)) + done + + MINLEVEL=${level} + toc_text=$(_generate_toc "${toc_temp_file}") + rm "${toc_temp_file}" + + _insert_toc_to_file "${source_markdown}" "${toc_text}" +} + +_generate_webdoc() { + declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file + declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc + declare webdoc_lastmod_date webdoc_lastmod_epoc + file="$(realpath "${1}")" + dest_dir="${2}" + + filename="${file##*/}" + file_basename="${filename%.*}" + dest_file_path="${dest_dir}/${file_basename}.md" + file_modified_date="$(date -r "${file}" +"%FT%T%:z")" + file_modified_date_epoc="$(date -r "${file}" +"%s")" + + start_shdoc="" + end_shdoc="" + if [[ ! -f "${dest_file_path}" ]]; then + + cat << EOF > "${dest_file_path}" +--- +title : +description : +date : ${file_modified_date} +lastmod : ${file_modified_date} +--- +${start_shdoc} +${end_shdoc} +EOF + is_new_file=true + else + is_new_file=false + webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")" + webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")" + fi + + if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then + + shdoc_tmp_file=$(_setup_tempfile) + if [[ -s "${file}" ]]; then + awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}" + fi + + if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then + sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}" + fi + + # Extract title and description from webdoc + title="$(sed -ne 's/-->//; s/^.*//; s/^.*/${title}/g" "${dest_file_path}" + sed -i -e "s//${description}/g" "${dest_file_path}" + else + sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}" + sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}" + + fi + + # Update the last modified timestamp in front matter + sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}" + + echo -e "Updated bashdoc content to ${dest_file_path} successfully." + rm "${shdoc_tmp_file}" + + fi +} +_process_webdoc_files() { + declare source_script_dir dest_dir + + source_script_dir="${1}" + dest_dir="${2}" + + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + while IFS= read -r -d '' line; do + _generate_webdoc "${line}" "${dest_dir}" + done +} + +_count_library_functions() { + declare source_script_dir + source_script_dir="${1}" + + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + { + declare -i function_count=0 count=0 + while IFS= read -r -d '' line; do + count=0 + count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}") + function_count=$((function_count + count)) + done + printf "Total library functions: %s \n" "${function_count}" + + } +} +main() { + # export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' + # set -x + trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT + set -o errexit -o noclobber -o pipefail + + _setup_arguments "${@}" + _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" + _process_toc "${SOURCE_MARKDOWN}" + + if [[ -n ${WEBDOC} ]]; then + _process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}" + fi + _count_library_functions "${SOURCE_SCRIPT_DIR}" +} + +main "${@}" diff --git a/src/configng/functions/bash-utility-master/image/bash-utility.png b/src/configng/functions/bash-utility-master/image/bash-utility.png new file mode 100644 index 0000000000000000000000000000000000000000..c1bfb8b556809ce645cf41ab6d4b11547df68fc7 GIT binary patch literal 32396 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 literal 0 HcmV?d00001 diff --git a/src/configng/functions/bash-utility-master/src/array.sh b/src/configng/functions/bash-utility-master/src/array.sh new file mode 100644 index 000000000..42d5883b8 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/array.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash + +# @file Array +# @brief Functions for array operations and manipulations. + +# @description Check if item exists in the given array. +# +# @example +# array=("a" "b" "c") +# array::contains "c" ${array[@]} +# #Output +# 0 +# +# @arg $1 mixed Item to search (needle). +# @arg $2 array array to be searched (haystack). +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found in the array. +# @exitcode 2 Function missing arguments. +array::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare query="${1:-}" + shift + + for element in "${@}"; do + [[ "${element}" == "${query}" ]] && return 0 + done + + return 1 +} + +# @description Remove duplicate items from the array. +# +# @example +# array=("a" "b" "a" "c") +# printf "%s" "$(array::dedupe ${array[@]})" +# #Output +# a +# b +# c +# +# @arg $1 array Array to be deduped. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Deduplicated array. +array::dedupe() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -A arr_tmp + declare -a arr_unique + for i in "$@"; do + { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue + arr_unique+=("${i}") && arr_tmp[${i}]=x + done + printf '%s\n' "${arr_unique[@]}" +} + +# @description Check if a given array is empty. +# +# @example +# array=("a" "b" "c" "d") +# array::is_empty "${array[@]}" +# +# @arg $1 array Array to be checked. +# +# @exitcode 0 If the given array is empty. +# @exitcode 2 If the given array is not empty. +array::is_empty() { + declare -a array + local array=("$@") + if [ ${#array[@]} -eq 0 ]; then + return 0 + else + return 1 + fi +} +# @description Join array elements with a string. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s" "$(array::join "," "${array[@]}")" +# #Output +# a,b,c,d +# printf "%s" "$(array::join "" "${array[@]}")" +# #Output +# abcd +# +# @arg $1 string String to join the array elements (glue). +# @arg $2 array array to be joined with glue string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. +array::join() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare delimiter="${1}" + shift + printf "%s" "${1}" + shift + printf "%s" "${@/#/${delimiter}}" +} + +# @description Return an array with elements in reverse order. +# +# @example +# array=(1 2 3 4 5) +# printf "%s" "$(array::reverse "${array[@]}")" +# #Output +# 5 4 3 2 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The reversed array. +array::reverse() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare min=0 + declare -a array + array=("$@") + declare max=$((${#array[@]} - 1)) + + while [[ $min -lt $max ]]; do + # Swap current first and last elements + x="${array[$min]}" + array[$min]="${array[$max]}" + array[$max]="$x" + + # Move closer + ((min++, max--)) + done + printf '%s\n' "${array[@]}" +} + +# @description Returns a random item from the array. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s\n" "$(array::random_element "${array[@]}")" +# #Output +# c +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Random item out of the array. +array::random_element() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array + local array=("$@") + printf '%s\n' "${array[RANDOM % $#]}" +} + +# @description Sort an array from lowest to highest. +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# 1 +# 2 +# 4 5 +# a +# a c +# d +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout sorted array. +array::sort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort <<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Sort an array in reverse order (highest to lowest). +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# d +# a c +# a +# 4 5 +# 2 +# 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout reverse sorted array. +array::rsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort -r<<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Bubble sort an integer array from lowest to highest. +# This sort does not work on string array. +# @example +# iarr=(4 5 1 3) +# array::bsort "${iarr[@]}" +# #Output +# 1 +# 3 +# 4 +# 5 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout bubble sorted array. +array::bsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare tmp + declare arr=("$@") + for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do + for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do + if [[ ${arr[i]} -gt ${arr[j]} ]]; then + # echo $i $j ${arr[i]} ${arr[j]} + tmp=${arr[i]} + arr[i]=${arr[j]} + arr[j]=$tmp + fi + done + done + printf "%s\n" "${arr[@]}" +} + +# @description Merge two arrays. +# Pass the variable name of the array instead of value of the variable. +# @example +# a=("a" "c") +# b=("d" "c") +# array::merge "a[@]" "b[@]" +# #Output +# a +# c +# d +# c +# +# @arg $1 string variable name of first array. +# @arg $2 string variable name of second array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Merged array. +array::merge() { + [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr1=("${!1}") + declare -a arr2=("${!2}") + declare out=("${arr1[@]}" "${arr2[@]}") + printf "%s\n" "${out[@]}" +} diff --git a/src/configng/functions/bash-utility-master/src/check.sh b/src/configng/functions/bash-utility-master/src/check.sh new file mode 100644 index 000000000..2b7c1eb1d --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/check.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# @file Check +# @brief Helper functions. + +# @description Check if the command exists in the system. +# +# @example +# check::command_exists "tput" +# +# @arg $1 string Command name to be searched. +# +# @exitcode 0 If the command exists. +# @exitcode 1 If the command does not exist. +# @exitcode 2 Function missing arguments. +check::command_exists() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + hash "${1}" 2> /dev/null +} + +# @description Check if the script is executed with sudo privilege. +# +# @example +# check::is_sudo +# +# @noargs +# +# @exitcode 0 If the script is executed with root privilege. +# @exitcode 1 If the script is not executed with root privilege +check::is_sudo() { + if [[ $(id -u) -ne 0 ]]; then + return 1 + fi +} diff --git a/src/configng/functions/bash-utility-master/src/collection.sh b/src/configng/functions/bash-utility-master/src/collection.sh new file mode 100644 index 000000000..9f0e244a2 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/collection.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash + +# @file Collection +# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. + +# @description Iterates over elements of collection and invokes iteratee for each element. +# Input to the function can be a pipe output, here-string or file. +# @example +# test_func(){ +# printf "print value: %s\n" "$1" +# return 0 +# } +# arr1=("a b" "c d" "a" "d") +# printf "%s\n" "${arr1[@]}" | collection::each "test_func" +# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach +# #output +# print value: a b +# print value: c d +# print value: a +# print value: d +# +# @example +# # If other function from this library is already used to process the array. +# # Then following method could be used to pass the array to the function. +# out=("$(array::dedupe "${arr1[@]}")") +# collection::each "test_func" <<< "${out[@]}" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output of iteratee function. +collection::each() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + done +} + +# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "4") +# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If iteratee function fails. +# @exitcode 2 Function missing arguments. +collection::every() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return 1 + fi + + done +} + +# @description Iterates over elements of array, returning all elements where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" +# #output +# 1 +# 2 +# 3 +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values matching the iteratee function. +collection::filter() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s\n" "${it}" + fi + done +} + +# @description Iterates over elements of collection, returning the first element where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("1" "2" "3" "a") +# check_a(){ +# [[ "$1" = "a" ]] +# } +# printf "%s\n" "${arr[@]}" | collection::find "check_a" +# #Output +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +# +# @stdout first array value matching the iteratee function. +collection::find() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s" "${it}" + return 0 + fi + done + + return 1 +} + +# @description Invokes the iteratee with each element passed as argument to the iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# opt=("-a" "-l") +# printf "%s\n" "${opt[@]}" | collection::invoke "ls" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output from the iteratee function. +collection::invoke() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a args=() + declare func="${1}" + while read -r it; do + args=("${args[@]}" "$it") + done + + eval "${func}" "${args[@]}" +} + +# @description Creates an array of values by running each element in array through iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3") +# add_one(){ +# i=${1} +# i=$(( i + 1 )) +# printf "%s\n" "$i" +# } +# printf "%s\n" "${arri[@]}" | collection::map "add_one" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output result of iteratee on value. +collection::map() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + declare out + + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + out="$("${func}")" + else + out="$("${func}" "$it")" + fi + + declare -i ret=$? + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + printf "%s\n" "${out}" + done +} + +# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" +# #Ouput +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values not matching the iteratee function. +# @see collection::filter +collection::reject() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + echo "$it" + fi + + done +} + +# @description Checks if iteratee returns true for any element of the array. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("a" "b" "3" "a") +# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If match successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +collection::some() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + + declare -i ret=$? + + if [[ $ret -eq 0 ]]; then + return 0 + fi + done + + return 1 +} diff --git a/src/configng/functions/bash-utility-master/src/date.sh b/src/configng/functions/bash-utility-master/src/date.sh new file mode 100644 index 000000000..41f071792 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/date.sh @@ -0,0 +1,744 @@ +#!/usr/bin/env bash + +# @file Date +# @brief Functions for manipulating dates. + +# @description Get current time in unix timestamp. +# +# @example +# echo "$(date::now)" +# #Output +# 1591554426 +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout current timestamp. +date::now() { + declare now + now="$(date --universal +%s)" || return $? + printf "%s" "${now}" +} + +# @description convert datetime string to unix timestamp. +# +# @example +# echo "$(date::epoc "2020-07-07 18:38")" +# #Output +# 1594143480 +# +# @arg $1 string date time in any format. +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp for specified datetime. +date::epoc() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare date + date=$(date -d "${1}" +"%s") || return $? + printf "%s" "${date}" +} + +# @description Add number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days_from "1594143480")" +# #Output +# 1594229880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months_from "1594143480")" +# #Output +# 1596821880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years_from "1594143480")" +# #Output +# 1625679480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::add_weeks_from "1594143480")" +# #Output +# 1594748280 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours_from "1594143480")" +# #Output +# 1594147080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes_from "1594143480")" +# #Output +# 1594143540 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds_from "1594143480")" +# #Output +# 1594143481 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days "1")" +# #Output +# 1591640826 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months "1")" +# #Output +# 1594146426 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years "1")" +# #Output +# 1623090426 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_weeks "1")" +# #Output +# 1592159226 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours "1")" +# #Output +# 1591558026 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes "1")" +# #Output +# 1591554486 +# +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds "1")" +# #Output +# 1591554427 +# +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days_from "1594143480")" +# #Output +# 1594057080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months_from "1594143480")" +# #Output +# 1591551480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years_from "1594143480")" +# #Output +# 1562521080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks_from "1594143480")" +# #Output +# 1593538680 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours_from "1594143480")" +# #Output +# 1594139880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes_from "1594143480")" +# #Output +# 1594143420 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds_from "1594143480")" +# #Output +# 1594143479 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days "1")" +# #Output +# 1588876026 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months "1")" +# #Output +# 1559932026 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years "1")" +# #Output +# 1591468026 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks "1")" +# #Output +# 1590949626 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours "1")" +# #Output +# 1591550826 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes "1")" +# #Output +# 1591554366 +# +# @arg $1 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds "1")" +# #Output +# 1591554425 +# +# @arg $1 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Format unix timestamp to human readable format. +# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. +# +# @example +# echo echo "$(date::format "1594143480")" +# #Output +# 2020-07-07 18:38:00 +# +# @arg $1 int unix timestamp. +# @arg $2 string format control characters based on `date` command (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate time string. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +date::format() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp format out + timestamp="${1}" + format="${2:-"%F %T"}" + out="$(date -d "@${timestamp}" +"${format}")" || return $? + printf "%s" "${out}" + +} diff --git a/src/configng/functions/bash-utility-master/src/debug.sh b/src/configng/functions/bash-utility-master/src/debug.sh new file mode 100644 index 000000000..82828734f --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/debug.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# @file Debug +# @brief Functions to facilitate debugging scripts. + +# @description Prints the content of array as key value pair for easier debugging. +# Pass the variable name of the array instead of value of the variable. +# @example +# array=(foo bar baz) +# printf "Array\n" +# printarr "array" +# declare -A assoc_array +# assoc_array=([foo]=bar [baz]=foobar) +# printf "Assoc Array\n" +# printarr "assoc_array" +# #Output +# Array +# 0 = foo +# 1 = bar +# 2 = baz +# Assoc Array +# baz = foobar +# foo = bar +# +# @arg $1 string variable name of the array. +# +# @stdout Formatted key value of array. +debug::print_array() { + declare -n __arr="$1" + for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done +} + +# @description Function to print ansi escape sequence as is. +# This function helps debug ansi escape sequence in text by displaying the escape codes. +# +# @example +# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" +# debug::print_ansi "$txt" +# #Output +# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m +# +# @arg $1 string input with ansi escape sequence. +# +# @stdout Ansi escape sequence printed in output as is. +debug::print_ansi() { + #echo $(tr -dc '[:print:]'<<<$1) + printf "%s\n" "${1//$'\e'/\\e}" + +} diff --git a/src/configng/functions/bash-utility-master/src/file.sh b/src/configng/functions/bash-utility-master/src/file.sh new file mode 100644 index 000000000..71afd8476 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/file.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash + +# @file File +# @brief Functions for handling files. + +# @description Create temporary file. +# Function creates temporary file with random name. The temporary file will be deleted when script finishes. +# +# @example +# echo "$(file::make_temp_file)" +# #Output +# tmp.vgftzy +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp file. +# +# @stdout file name of temporary file created. +file::make_temp_file() { + declare temp_file + type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } + trap 'rm -f "${temp_file}"' EXIT + printf "%s" "${temp_file}" +} + +# @description Create temporary directory. +# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. +# +# @example +# echo "$(utility::make_temp_dir)" +# #Output +# tmp.rtfsxy +# +# @arg $1 string Temporary directory prefix +# @arg $2 string Flag to auto remove directory on exit trap (true) +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp directory. +# @exitcode 2 Missing arguments. +# +# @stdout directory name of temporary directory created. +file::make_temp_dir() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare temp_dir prefix="${1}" trap_rm="${2}" + temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") + if [[ -n "${trap_rm}" ]]; then + trap 'rm -rf "${temp_dir}"' EXIT + fi + printf "%s" "${temp_dir}" +} + +# @description Get only the filename from string path. +# +# @example +# echo "$(file::name "/path/to/test.md")" +# #Output +# test.md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout name of the file with extension. +file::name() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf "%s" "${1##*/}" +} + +# @description Get the basename of file from file name. +# +# @example +# echo "$(file::basename "/path/to/test.md")" +# #Output +# test +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout basename of the file. +file::basename() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare file basename + file="${1##*/}" + basename="${file%.*}" + + printf "%s" "${basename}" +} + +# @description Get the extension of file from file name. +# +# @example +# echo "$(file::extension "/path/to/test.md")" +# #Output +# md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 1 If no extension is found in the filename. +# @exitcode 2 Function missing arguments. +# +# @stdout extension of the file. +file::extension() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare file extension + file="${1##*/}" + extension="${file##*.}" + [[ "${file}" = "${extension}" ]] && return 1 + + printf "%s" "${extension}" +} + +# @description Get directory name from file path. +# +# @example +# echo "$(file::dirname "/path/to/test.md")" +# #Output +# /path/to +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout directory path. +file::dirname() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare tmp=${1:-.} + + [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } + tmp="${tmp%%"${tmp##*[!/]}"}" + + [[ ${tmp} != */* ]] && { printf '.\n' && return; } + tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" + + printf '%s' "${tmp:-/}" +} + +# @description Get absolute path of file or directory. +# +# @example +# file::full_path "../path/to/file.md" +# #Output +# /home/labbots/docs/path/to/file.md +# +# @arg $1 string relative or absolute path to file/direcotry. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# +# @stdout Absolute path to file/directory. +file::full_path() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare input="${1}" + if [[ -f ${input} ]]; then + printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" + elif [[ -d ${input} ]]; then + printf "%s\n" "$(cd "${input}" && pwd)" + else + return 1 + fi +} + +# @description Get mime type of provided input. +# +# @example +# file::mime_type "../src/file.sh" +# #Output +# application/x-shellscript +# +# @arg $1 string relative or absolute path to file/directory. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# @exitcode 3 If file or mimetype command not found in system. +# +# @stdout mime type of file/directory. +file::mime_type() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare mime_type + if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then + if type -p mimetype &> /dev/null; then + mime_type=$(mimetype --output-format %m "${1}") + elif type -p file &> /dev/null; then + mime_type=$(file --brief --mime-type "${1}") + else + return 3 + fi + else + return 1 + fi + printf "%s" "${mime_type}" +} + +# @description Search if a given pattern is found in file. +# +# @example +# file::contains_text "./file.sh" "^[ @[:alpha:]]*" +# file::contains_text "./file.sh" "@file" +# #Output +# 0 +# +# @arg $1 string relative or absolute path to file/directory. +# @arg $2 string search key or regular expression. +# +# @exitcode 0 If given search parameter is found in file. +# @exitcode 1 If search paramter not found in file. +# @exitcode 2 Function missing arguments. +file::contains_text() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + declare -r file="$1" + declare -r text="$2" + grep -q "$text" "$file" +} diff --git a/src/configng/functions/bash-utility-master/src/format.sh b/src/configng/functions/bash-utility-master/src/format.sh new file mode 100644 index 000000000..7dceb1d59 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/format.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash + +# @file Format +# @brief Functions to format provided input. + +# @internal +# @description Initialisation script when the code is sourced. +# +# @noargs +__init(){ +_check_terminal_window_size +} + +# @internal +# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. +# +# @noargs +_check_terminal_window_size() { + shopt -s checkwinsize && (: && :) + trap 'shopt -s checkwinsize; (:;:)' SIGWINCH +} +# @description Format seconds to human readable format. +# +# @example +# echo "$(format::human_readable_seconds "356786")" +# #Output +# 4 days 3 hours 6 minute(s) and 26 seconds +# +# @arg $1 int number of seconds. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +format::human_readable_seconds() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare T="${1}" + declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" + [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" + [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" + [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" + [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' + printf '%d seconds\n' "${SEC}" +} + +# @description Format bytes to human readable format. +# +# @example +# echo "$(format::bytes_to_human "2250")" +# #Output +# 2.19 KB +# +# @arg $1 int size in bytes. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted file size string. +format::bytes_to_human() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) + while ((b > 1024)); do + d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" + b=$((b / 1024)) && ((s++)) + done + printf "%s\n" "${b}${d} ${S[${s}]}" +} + +# @description Remove Ansi escape sequences from given text. +# +# @example +# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" +# #Output +# This is bold red text.This is green text. +# +# @arg $1 string Input text to be ansi stripped. +# +# @exitcode 0 If successful. +# +# @stdout Ansi stripped text. +format::strip_ansi() { + declare tmp esc tpa re + tmp="${1}" + esc=$(printf "\x1b") + tpa=$(printf "\x28") + re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" + while [[ "${tmp}" =~ $re ]]; do + tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" + done + printf "%s" "${tmp}" +} + +# @description Prints the given text to centre of terminal. +# +# @example +# format::text_center "This text is in centre of the terminal." "-" +# +# @arg $1 string Text to be printed. +# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::text_center() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare input="${1}" symbol="${2:- }" filler out no_ansi_out + no_ansi_out=$(format::strip_ansi "$input") + declare -i str_len=${#no_ansi_out} + declare -i filler_len="$(((COLUMNS - str_len) / 2))" + + [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" + for ((i = 0; i < filler_len; i++)); do + filler+="${symbol}" + done + + out="${filler}${input}${filler}" + [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" + printf "%s" "${out}" +} + +# @description Format String to print beautiful report. +# +# @example +# format::report "Initialising mission state" "Success" +# #Output +# Initialising mission state ....................................................................[ Success ] +# +# @arg $1 string Text to be printed on the left. +# @arg $2 string Text to be printed within the square brackets. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::report() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare symbol="." to_print y hl hlout out + declare input1="${1} " input2="${2}" + input2="[ $input2 ]" + to_print="$((COLUMNS * 60 / 100))" + y=$(( to_print - ( ${#input1} + ${#input2} ) )) + hl="$(printf '%*s' $y '')" + hlout=${hl// /${symbol}} + out="${input1}${hlout}${input2}" + printf "%s\n" "${out}" +} + +# @description Trim given text to width of the terminal window. +# +# @example +# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." +# #Output +# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. +# +# @arg $1 string Text of first sentence. +# @arg $2 string Text of second sentence (optional). +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout trimmed text. +format::trim_text_to_term() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare to_print out input1="$1" input2="$2" + if [[ $# = 1 ]]; then + to_print="$((COLUMNS * 93 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } + else + to_print="$((COLUMNS * 40 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } + to_print="$((COLUMNS * 53 / 100))" + { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } + fi + printf "%s" "$out" +} + +__init diff --git a/src/configng/functions/bash-utility-master/src/interaction.sh b/src/configng/functions/bash-utility-master/src/interaction.sh new file mode 100644 index 000000000..910b60e1c --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/interaction.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# @file Interaction +# @brief Functions to enable interaction with the user. + +# @description Prompt yes or no question to the user. +# +# @example +# interaction::prompt_yes_no "Are you sure to proceed" "yes" +# #Output +# Are you sure to proceed (y/n)? [y] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer \[yes/no\] (optional). +# +# @exitcode 0 If user responds with yes. +# @exitcode 1 If user responds with no. +# @exitcode 2 Function missing arguments. +interaction::prompt_yes_no() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare def_arg response + def_arg="" + response="" + + case "${2}" in + [yY] | [yY][eE][sS]) + def_arg=y + ;; + [nN] | [nN][oO]) + def_arg=n + ;; + esac + + while :; do + printf "%s (y/n)? " "${1}" + [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -z "${response}" ]] && response="${def_arg}" + + case "${response}" in + [yY] | [yY][eE][sS]) + response=y + break + ;; + [nN] | [nN][oO]) + response=n + break + ;; + *) + response="" + ;; + esac + done + + [[ "${response}" = 'y' ]] && return 0 || return 1 +} + +# @description Prompt question to the user. +# +# @example +# interaction::prompt_response "Choose directory to install" "/home/path" +# #Output +# Choose directory to install? [/home/path] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer (optional). +# +# @exitcode 0 If user responds with answer. +# @exitcode 2 Function missing arguments. +# +# @stdout User entered answer to the question. +interaction::prompt_response() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare def_arg response + response="" + def_arg="${2}" + + while :; do + printf "%s ? " "${1}" + [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -n "${response}" ]] && break + + if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then + response="${def_arg}" + break + fi + done + + [[ "${response}" = "-" ]] && response="" + + printf "%s" "${response}" +} diff --git a/src/configng/functions/bash-utility-master/src/json.sh b/src/configng/functions/bash-utility-master/src/json.sh new file mode 100644 index 000000000..73476618e --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/json.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# @file Json +# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. + +# @description Extract value from json based on key and position. +# Input to the function can be a pipe output, here-string or file. +# @example +# json::get_value "id" "1" < json_file +# json::get_value "id" <<< "${json_var}" +# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" +# +# @arg $1 string id of the field to fetch. +# @arg $2 int position of value to extract.Defaults to 1.(optional) +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string value of extracted key. +json::get_value() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare LC_ALL=C num="${2:-1}" + grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p +} diff --git a/src/configng/functions/bash-utility-master/src/misc.sh b/src/configng/functions/bash-utility-master/src/misc.sh new file mode 100644 index 000000000..fca9a75bf --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/misc.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# @file Miscellaneous +# @brief Set of miscellaneous helper functions. + +# @internal +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +_is_terminal() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Check if internet connection is available. +# +# @example +# misc::check_internet_connection +# +# @noargs +# +# @exitcode 0 If script can connect to internet. +# @exitcode 1 If script cannot access internet. +misc::check_internet_connection() { + declare check_internet + if _is_terminal; then + check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" + else + check_internet="$(curl --compressed -Is google.com -m 10)" + fi + if [[ -z ${check_internet} ]]; then + return 1 + fi +} + +# @description Get list of process ids based on process name. +# +# @example +# misc::get_pid "chrome" +# #Ouput +# 25951 +# 26043 +# 26528 +# 26561 +# +# @arg $1 Name of the process to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout list of process ids. +misc::get_pid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + pgrep "${1}" +} + +# @description Get user id based on username. +# +# @example +# misc::get_uid "labbots" +# #Ouput +# 1000 +# +# @arg $1 username to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string uid for the username. +misc::get_uid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + user_id=$(id "${1}" 2> /dev/null) + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + printf "No user found with username: %s" "${1}\n" + return 1 + fi + + printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' + + unset user_id +} + +# @description Generate random uuid. +# +# @example +# misc::generate_uuid +# #Ouput +# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c +# +# @noargs +# +# @exitcode 0 If match successful. +# +# @stdout random generated uuid. +misc::generate_uuid() { + C="89ab" + + for ((N=0;N<16;++N)); do + B="$((RANDOM%256))" + + case "$N" in + 6) printf '4%x' "$((B%16))" ;; + 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; + + 3|5|7|9) + printf '%02x-' "$B" + ;; + + *) + printf '%02x' "$B" + ;; + esac + done + + printf '\n' +} diff --git a/src/configng/functions/bash-utility-master/src/os.sh b/src/configng/functions/bash-utility-master/src/os.sh new file mode 100644 index 000000000..78e0d36c5 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/os.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +# @file Operating System +# @brief Functions to detect Operating system and version. + +# @description Identify the OS the function is run on. +# +# @noargs +# +# @example +# os::detect_os +# #Output +# linux +# +# @exitcode 0 If OS is successfully detected. +# @exitcode 1 If unable to detect OS. +# +# @stdout Operating system name (linux, mac or windows). +os::detect_os() { + declare uname os + uname=$(command -v uname) + + case $("${uname}" | tr '[:upper:]' '[:lower:]') in + linux*) + os="linux" + ;; + darwin*) + os="mac" + ;; + msys* | cygwin* | mingw* | nt | win*) + # or possible 'bash on windows' + os="windows" + ;; + *) + return 1 + ;; + esac + printf "%s" "${os}" +} + +# @description Identify the distribution flavour of linux. +# +# @noargs +# +# @example +# os::detect_linux_distro +# #Output +# ubuntu +# @exitcode 0 If Linux distro is successfully detected. +# @exitcode 1 If unable to detect OS distro. +# +# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). +os::detect_linux_distro() { + declare distro + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro="${NAME}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro=$(lsb_release -si) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro="${DISTRIB_ID}" + elif [[ -f /etc/debian_version ]]; then + # Older Debian/Ubuntu/etc. + distro="debian" + elif [[ -f /etc/SuSe-release ]]; then + # Older SuSE/etc. + distro="suse" + elif [[ -f /etc/redhat-release ]]; then + # Older Red Hat, CentOS, etc. + distro="redhat" + else + return 1 + fi + printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' +} + +# @description Identify the Linux version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 20.04 +# +# @exitcode 0 If Linux version is successfully detected. +# @exitcode 1 If unable to detect Linux version. +# +# @stdout Linux OS version number (18.04, 20.04, etc.,). +os::detect_linux_version() { + declare distro_version + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro_version="${VERSION_ID}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro_version=$(lsb_release -sr) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro_version="${DISTRIB_RELEASE}" + else + return 1 + fi + printf "%s" "${distro_version}" +} + +# @description Identify the Linux codename. +# +# @noargs +# +# @example +# os::detect_linux_codename +# #Output +# jammy +# +# @exitcode 0 If Linux codename is successfully detected. +# @exitcode 1 If unable to detect Linux codename. +# +# @stdout Linux OS codename number (buster, jammy, etc.,). +os::detect_linux_codename() { + declare distro_codename + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro_codename="${VERSION_CODENAME}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro_codename=$(lsb_release -cs) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro_codename="${DISTRIB_CODENAME}" + else + return 1 + fi + printf "%s" "${distro_codename}" +} + +# @description Identify the MacOS version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 10.15.7 +# @exitcode 0 If MacOS version is successfully detected. +# @exitcode 1 If unable to detect MacOS version. +# +# @stdout MacOS version number (10.15.6, etc.,) +os::detect_mac_version() { + if [[ "$(os::detect_os)" = "mac" ]]; then + declare mac_version + mac_version="$(sw_vers -productVersion)" + printf "%s" "${mac_version}" + else + return 1 + fi +} diff --git a/src/configng/functions/bash-utility-master/src/string.sh b/src/configng/functions/bash-utility-master/src/string.sh new file mode 100644 index 000000000..aa522cb55 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/string.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash + +# @file String +# @brief Functions for string operations and manipulations. + +# @description Strip whitespace from the beginning and end of a string. +# +# @example +# echo "$(string::trim " Hello World! ")" +# #Output +# Hello World! +# +# @arg $1 string The string to be trimmed. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The trimmed string. +string::trim() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + : "${1#"${1%%[![:space:]]*}"}" + : "${_%"${_##*[![:space:]]}"}" + printf '%s\n' "$_" +} + +# @description Split a string to array by a delimiter. +# +# @example +# array=( $(string::split "a,b,c" ",") ) +# printf "%s" "$(string::split "Hello!World" "!")" +# #Output +# Hello +# World +# +# @arg $1 string The input string. +# @arg $2 string The delimiter string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. +string::split() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr=() + IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" + printf '%s\n' "${arr[@]}" +} + +# @description Strip characters from the beginning of a string. +# +# @example +# echo "$(string::lstrip "Hello World!" "He")" +# #Output +# llo World! +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::lstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1##$2}" +} + +# @description Strip characters from the end of a string. +# +# @example +# echo "$(string::rstrip "Hello World!" "d!")" +# #Output +# Hello Worl +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::rstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1%%$2}" +} + +# @description Make a string lowercase. +# +# @example +# echo "$(string::to_lower "HellO")" +# #Output +# hello +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the lowercased string. +string::to_lower() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1,,}" + else + printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' + fi +} + +# @description Make a string all uppercase. +# +# @example +# echo "$(string::to_upper "HellO")" +# #Output +# HELLO +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the uppercased string. +string::to_upper() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1^^}" + else + printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' + fi +} + +# @description Check whether the search string exists within the input string. +# +# @example +# string::contains "Hello World!" "lo" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_contains hello he + [[ "${1}" == *${2}* ]] +} + +# @description Check whether the input string starts with key string. +# +# @example +# string::starts_with "Hello World!" "He" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::starts_with() { + # Usage: string_starts_with hello he + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + [[ "${1}" == ${2}* ]] +} + +# @description Check whether the input string ends with key string. +# +# @example +# string::ends_with "Hello World!" "d!" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::ends_with() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_ends_wit hello lo + [[ "${1}" == *${2} ]] +} + +# @description Check whether the input string matches the given regex. +# +# @example +# string::regex "HELLO" "^[A-Z]*$" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::regex() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${1} =~ ${2} ]]; then + return 0 + else + return 1 + fi + +} diff --git a/src/configng/functions/bash-utility-master/src/terminal.sh b/src/configng/functions/bash-utility-master/src/terminal.sh new file mode 100644 index 000000000..d73331d7a --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/terminal.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# @file Terminal +# @brief Set of useful terminal functions. + +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +terminal::is_term() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Detect profile rc file for zsh and bash of current script running user. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +# +# @stdout path to the profile file. +terminal::detect_profile() { + declare CURRENT_SHELL="${SHELL##*/}" + case "${CURRENT_SHELL}" in + 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; + 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; + *) if [[ -f "${HOME}/.profile" ]]; then + DETECTED_PROFILE="${HOME}/.profile" + else + printf "No compaitable shell file\n" && exit 1 + fi ;; + esac + printf "%s\n" "${DETECTED_PROFILE}" +} + +# @description Clear the output in terminal on the specified line number. +# This function clears line only on terminal. +# +# @arg $1 Line number to clear. Defaults to 1. (optional) +# +# @exitcode 0 If script is run on terminal. +# +# @stdout clear line ansi code. +terminal::clear_line() { + if terminal::is_term; then + declare line=${1:-1} + printf "\033[%sA\033[2K" "${line}" + fi +} diff --git a/src/configng/functions/bash-utility-master/src/validation.sh b/src/configng/functions/bash-utility-master/src/validation.sh new file mode 100644 index 000000000..37fde1cbc --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/validation.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash + +# @file Validation +# @brief Functions to perform validation on given data. + +# @description Validate whether a given input is a valid email address or not. +# +# @example +# test='test@gmail.com' +# validation::email "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string input email address to validate. +# +# @exitcode 0 If provided input is an email address. +# @exitcode 1 If provided input is not an email address. +# @exitcode 2 Function missing arguments. +validation::email() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare email_re + email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" + [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 +} + +# @description Validate whether a given input is a valid IP V4 address. +# +# @example +# ips=' +# 4.2.2.2 +# a.b.c.d +# 192.168.1.1 +# 0.0.0.0 +# 255.255.255.255 +# 255.255.255.256 +# 192.168.0.1 +# 192.168.0 +# 1234.123.123.123 +# 0.192.168.1 +# ' +# for ip in $ips; do +# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi +# printf "%-20s: %s\n" "$ip" "$stat" +# done +# #Output +# 4.2.2.2 : good +# a.b.c.d : bad +# 192.168.1.1 : good +# 0.0.0.0 : good +# 255.255.255.255 : good +# 255.255.255.256 : bad +# 192.168.0.1 : good +# 192.168.0 : bad +# 1234.123.123.123 : bad +# 0.192.168.1 : good +# +# @arg $1 string input IPv4 address. +# +# @exitcode 0 If provided input is a valid IPv4. +# @exitcode 1 If provided input is not a valid IPv4. +# @exitcode 2 Function missing arguments. +validation::ipv4() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare ip="${1}" + declare IFS=. + # shellcheck disable=SC2206 + declare -a a=($ip) + [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 + # Test values of quads + declare quad + for quad in {0..3}; do + [[ "${a[$quad]}" -gt 255 ]] && return 1 + done + return 0 +} + +# @description Validate whether a given input is a valid IP V6 address. +# +# @example +# ips=' +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 +# fe80::1ff:fe23:4567:890a +# fe80::1ff:fe23:4567:890a%eth2 +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar +# fezy::1ff:fe23:4567:890a +# :: +# 2001:db8:: +# ' +# for ip in $ips; do +# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi +# printf "%-50s= %s\n" "$ip" "$stat" +# done +# #Output +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good +# fe80::1ff:fe23:4567:890a = good +# fe80::1ff:fe23:4567:890a%eth2 = good +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad +# fezy::1ff:fe23:4567:890a = bad +# :: = good +# 2001:db8:: = good +# +# @arg $1 string input IPv6 address. +# +# @exitcode 0 If provided input is a valid IPv6. +# @exitcode 1 If provided input is not a valid IPv6. +# @exitcode 2 Function missing arguments. +validation::ipv6() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare ip="${1}" + declare 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]))$" + + [[ "${ip}" =~ $re ]] && return 0 || return 1 +} + +# @description Validate if given variable is entirely alphabetic characters. +# +# @example +# test='abcABC' +# validation::alpha "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is only alpha characters. +# @exitcode 1 If input contains any non alpha characters. +# @exitcode 2 Function missing arguments. +validation::alpha() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable contains only alpha-numeric characters. +# +# @example +# test='abc123' +# validation::alpha_num "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is an alpha-numeric. +# @exitcode 1 If input is not an alpha-numeric. +# @exitcode 2 Function missing arguments. +validation::alpha_num() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alnum:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. +# +# @example +# test='abc-ABC_cD' +# validation::alpha_dash "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is valid. +# @exitcode 1 If input the input is not valid. +# @exitcode 2 Function missing arguments. +validation::alpha_dash() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]_-]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. +# +# @arg $1 string Version number to check (eg: 1.0.1) +# $arg $2 string Version number to check (eg: 1.0.1) +# +# @example +# test='abc-ABC_cD' +# validation::version_comparison "12.0.1" "12.0.1" +# echo $? +# #Output +# 0 +# +# @exitcode 0 version number is equal. +# @exitcode 1 $1 version number is greater than $2. +# @exitcode 2 $1 version number is less than $2. +# @exitcode 3 Function is missing required arguments. +# @exitcode 4 Provided input argument is in invalid format. +validation::version_comparison() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 + + declare regex="^[.0-9]*$" + ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 + ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 + + if [[ "$1" == "$2" ]]; then + return 0 + fi + declare IFS=. + declare -a ver1 ver2 + read -r -a ver1 <<<"${1}" + read -r -a ver2 <<<"${2}" + # fill empty fields in ver1 with zeros + for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do + ver1[i]=0 + done + for ((i = 0; i < ${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + # fill empty fields in ver2 with zeros + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 + fi + done + return 0 +} diff --git a/src/configng/functions/bash-utility-master/src/variable.sh b/src/configng/functions/bash-utility-master/src/variable.sh new file mode 100644 index 000000000..67e9fab55 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/variable.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# @file Variable +# @brief Functions for handling variables. + +# @description Check if given variable is array. +# Pass the variable name instead of value of the variable. +# +# @example +# arr=("a" "b" "c") +# variable::is_array "arr" +# #Output +# 0 +# +# @arg $1 string name of the variable to check. +# +# @exitcode 0 If input is array. +# @exitcode 1 If input is not an array. +variable::is_array() { + if [[ -z "${1}" ]]; then + return 1 + else + declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 + fi + return 1 +} + +# @description Check if given variable is a number. +# +# @example +# variable::is_numeric "1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is number. +# @exitcode 1 If input is not a number. +variable::is_numeric() { + declare re='^[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is an integer. +# +# @example +# variable::is_int "+1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is an integer. +# @exitcode 1 If input is not an integer. +variable::is_int() { + declare re='^[+-]?[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a float. +# +# @example +# variable::is_float "+1234.0" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a float. +# @exitcode 1 If input is not a float. +variable::is_float() { + declare re='^[+-]?[0-9]+.?[0-9]*$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a boolean. +# +# @example +# variable::is_bool "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a boolean. +# @exitcode 1 If input is not a boolean. +variable::is_bool() { + [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 +} + +# @description Check if given variable is a true. +# +# @example +# variable::is_true "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is true. +# @exitcode 1 If input is not true. +variable::is_true() { + [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 +} + +# @description Check if given variable is false. +# +# @example +# variable::is_false "false" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is false. +# @exitcode 1 If input is not false. +variable::is_false() { + [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 +} + +# @description Check if given variable is empty or null. +# +# @example +# test='' +# variable::is_empty_or_null $test +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is empty or null. +# @exitcode 1 If input is not empty or null. +variable::is_empty_or_null() { + [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 +} diff --git a/functions/cpu.sh b/src/configng/functions/cpu.sh similarity index 100% rename from functions/cpu.sh rename to src/configng/functions/cpu.sh diff --git a/test/cpu_test.sh b/src/configng/test/cpu_test.sh old mode 100755 new mode 100644 similarity index 100% rename from test/cpu_test.sh rename to src/configng/test/cpu_test.sh From 64088d1059f603bee04c2a84dcf618010915e1a6 Mon Sep 17 00:00:00 2001 From: tearran Date: Wed, 12 Jul 2023 18:59:24 -0700 Subject: [PATCH 02/48] moved options --- bin/cpu_test.sh | 114 +----------------------------------------------- 1 file changed, 1 insertion(+), 113 deletions(-) diff --git a/bin/cpu_test.sh b/bin/cpu_test.sh index 9a50d4954..2390654e5 100755 --- a/bin/cpu_test.sh +++ b/bin/cpu_test.sh @@ -58,7 +58,7 @@ check_return(){ fi } -see_cpu(){ + # Get policy declare -i policy=$(cpu::get_policy) printf 'Policy = %d\n' "$policy" @@ -100,115 +100,3 @@ cpu::set_freq $policy "$min_freq" "$max_freq" performance printf "\nAfter\n" cat /etc/default/cpufrequtils -} - -readarray -t functionarray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh") -readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh" | sed 's/.*:://') -readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$libpat/configng/cpu.sh" | sed 's/::.*//') -readarray -t descriptionarray < <(grep -oP '^# @description.*' "$libpath/configng/cpu.sh" | sed 's/^# @description //') - -#printf '%s\n' "${functionarray[@]}" -#exit 0 -see_help(){ - - echo "" - echo "Usage: ${filename%.*} [ -h | -dev | ]" - echo -e "Options:" - echo -e " -h Print this help." - echo -e " -dev Options:" - for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" - done - echo -e " -cpu Options:" - for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s\n' "${funnamearray[i]}" "${descriptionarray[i]}" - done - - } -# check for -dev -h options -check_opts_test1() -{ - if [[ "$1" == -dev ]] ; then - default="bash" - local found=false - for i in "${!functionarray[@]}"; do - if [ "$2" == "${functionarray[i]}" ]; then - "${functionarray[i]}" - found=true - break - fi - done - if ! $found; then - see_help - exit 0 - fi - elif [[ "$1" == "-cpu" ]] ; then - echo -e " " - echo -e " TODO:" - for i in "${!functionarray[@]}"; do - - printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" - done - - elif [[ "$1" == -h ]] ; then - see_help - else - see_cpu - fi -} - -# check for -h -dev -# if -dev check for number -check_opts_test2(){ - -if [ "$1" == "-dev" ]; then - shift # Shifts the arguments to the left, excluding the first argument ("-dev") - function_name="$1" # Assigns the next argument as the function name - shift # Shifts the arguments again to exclude the function name - - case "$function_name" in - 0) echo "Calling function 0 with arguments: $@" ;; - 1) echo "Calling function 1 with arguments: $@" ;; - 2) echo "Calling function 2 with arguments: $@" ;; - 3) echo "Calling function 3 with arguments: $@" ;; - 4) echo "Calling function 4 with arguments: $@" ;; - *) echo "Invalid function name" ;; - esac -else - echo "No -dev flag found" -fi - -} -# check for -h -dev @ $1 -# if -dev check @ $1 remove and shift $1 to check for x -check_opts() { - if [ "$1" == "-dev" ]; then - shift # Shifts the arguments to the left, excluding the first argument ("-dev") - function_name="$1" # Assigns the next argument as the function name - shift # Shifts the arguments again to exclude the function name - - found=false - - for ((i=0; i<${#functionarray[@]}; i++)); do - if [ "$function_name" == "${functionarray[i]}" ]; then - found=true - ${functionarray[i]} "$@" - break - fi - done - - if [ "$found" == false ]; then - echo "Invalid function name" - fi - - elif [ "$1" == "-h" ]; then - see_help - else - see_cpu - fi -} - -#check_opts_test1 "$@" -check_opts "$@" - -#cpu::set_freq $policy "$min_freq" "$max_freq" performance From ac1ab2d50261e1e09fe93b56b2f9c995eac3441d Mon Sep 17 00:00:00 2001 From: tearran Date: Fri, 14 Jul 2023 21:53:35 -0700 Subject: [PATCH 03/48] removed test --- bin/jampi-config | 212 ----------------------------------------------- 1 file changed, 212 deletions(-) delete mode 100755 bin/jampi-config diff --git a/bin/jampi-config b/bin/jampi-config deleted file mode 100755 index 4dd17d7c6..000000000 --- a/bin/jampi-config +++ /dev/null @@ -1,212 +0,0 @@ -#!/bin/bash - -# -# Copyright (c) 2023 Joseph C Turner -# All rights reserved. -# -# This script. -# demonstrates the compatibility of multiple interfaces for displaying menus or messages. -# It uses an array to set the options for all three menus (bash, whiptail, and dialog). -# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. -# If neither of these programs is available, it falls back to using bash. -# while both are installed falls back to whiptail to display the menu. -# The user can override the default program by passing an argument when running the script: -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# - - - -#DIRECTORY variable to the absolute path of the script's directory -#directory=$(cd "$(dirname "$0")" && pwd) -directory="$(dirname "$(readlink -f "$0")")" -filename=$(basename "${BASH_SOURCE[0]}") -selfpath="$directory"/"$filename" -#libpath="${directory}"/"its-lib" #Include these scripts Library -libpath="$selfpath" - - -clear -# Check for the availability of whiptail and dialog command-line programs -# Set the default program to use for displaying messages -( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" -( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" -( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" - -# if both whiptail and dialog change to prefered -( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" - - -[[ "$1" == -b ]] && default="bash" -[[ "$1" == -w ]] && default="whiptail" -[[ "$1" == -n ]] && default="dialog" - -[[ "$1" == -m ]] && { -export NEWT_COLORS=" -root=blue,black -border=green,black -title=green,black -roottext=red,black -window=red,black -textbox=white,black -button=black,green -compactbutton=white,black -listbox=white,black -actlistbox=black,white -actsellistbox=black,green -checkbox=green,black -actcheckbox=black,green -" -} - -# Check the cpu architecture. for later handeling if nessery -architecture=$(dpkg --print-architecture) -[[ "$architecture" == "armf" ]] && true ; - -#setup menu arrays -readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) -readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) -readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) - -## System@Settings:Advanced Settings (armbian-config) -cmd_advance() -{ - sudo armbian-config -} - -## System@CPU info:Example from Bash Utility (cpu_test.sh) -cmd_cpu_info() -{ - [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; - [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - - -## System@CPU info:An example function (lscpu) -cmd_cpu_ls() -{ - - shell_command="$(lscpu)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - -## System@Bootup Time:An example function (systemd-analyze time) -cmd_boot_time() -{ - - shell_command="$(systemd-analyze time)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - -## System@Login Info :An example function (Login Info) -cmd_lslogins() -{ - - shell_command="$(lslogins)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - - -# Function to display a message using whiptail, dialog or printf depending on what is available on the system -see_message() -{ - # Use if neither whiptail nor dialog are available on the system - if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then - # Use printf to display the message - printf '%s ' "${shell_command[@]}" - - # Use as default if whiptail is available - elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then - # Use whiptail to display the message - whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext - - # Use if dialog is available on the system but not whiptail - elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then - # Use dialog to display the message - dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear - fi -} - -see_menu() -{ - - if [[ "$default" == "bash" ]]; then - PS3="Enter a number: " - select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done - - elif [[ "$default" == "whiptail" ]]; then - OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) - [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" - - elif [[ "$default" == "dialog" ]]; then - OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) - [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" - fi -} - -see_help(){ - - echo "" - echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" - echo -e "Options:" - echo -e " -h Print this help." - echo -e " -b GNU bash " - echo -e " -n NCURSES dialog " - echo -e " -w NEWT whiptail - default colors " - echo -e " -m dark mode whiptail " - echo -e " -dev Options:" - for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" - done - - } - -main() -{ - if [[ "$1" == --dev ]] ; then - default="bash" - local found=false - for i in "${!functionarray[@]}"; do - if [ "$2" == "${functionarray[i]}" ]; then - "${functionarray[i]}" - found=true - break - fi - done - if ! $found; then - see_help - exit 0 - fi - elif [[ "$1" == -h ]] ; then - see_help - else - see_menu - fi -} - -main "$@" From 46991bc8f49bcf4dc0013125e3d3d41d986fc78c Mon Sep 17 00:00:00 2001 From: tearran Date: Fri, 14 Jul 2023 22:22:29 -0700 Subject: [PATCH 04/48] renamed: bin/cli_options.sh -> bin/configng.sh deleted: bin/cpu_test.sh new file: bin/jampi-config modified: lib/configng/cpu.sh new file: lib/configng/storage.sh --- bin/{cli_options.sh => configng.sh} | 53 +++++-- bin/cpu_test.sh | 102 ------------- bin/jampi-config | 212 ++++++++++++++++++++++++++++ lib/configng/cpu.sh | 69 +-------- lib/configng/storage.sh | 69 +++++++++ 5 files changed, 320 insertions(+), 185 deletions(-) rename bin/{cli_options.sh => configng.sh} (57%) delete mode 100755 bin/cpu_test.sh create mode 100644 bin/jampi-config create mode 100644 lib/configng/storage.sh diff --git a/bin/cli_options.sh b/bin/configng.sh similarity index 57% rename from bin/cli_options.sh rename to bin/configng.sh index 0062725b6..136a0409d 100644 --- a/bin/cli_options.sh +++ b/bin/configng.sh @@ -12,28 +12,51 @@ directory="$(dirname "$(readlink -f "$0")")" filename=$(basename "${BASH_SOURCE[0]}") libpath="$directory/../lib" -selfpath="$libpath/configng/cpu.sh" +#selfpath="$libpath/configng/cpu.sh" if [[ -d "$directory/../lib" ]]; then libpath="$directory"/../lib -elif [[ -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then - libpath="/usr/lib" +#elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then +# libpath="/usr/lib" else echo "Libraries not found" exit 0 fi +## Source the files relative to the script location +#source "$libpath/bash-utility/string.sh" +#source "$libpath/bash-utility/collection.sh" +#source "$libpath/bash-utility/array.sh" +#source "$libpath/bash-utility/check.sh" +#source "$libpath/configng/cpu.sh" + # Source the files relative to the script location -source "$libpath/bash-utility/string.sh" -source "$libpath/bash-utility/collection.sh" -source "$libpath/bash-utility/array.sh" -source "$libpath/bash-utility/check.sh" -source "$libpath/configng/cpu.sh" +for file in "$libpath"/bash-utility/*; do + source "$file" +done +for file in "$libpath"/configng/*; do + source "$file" +done + +functionarray=() +funnamearray=() +catagoryarray=() +descriptionarray=() + +for file in "$libpath"/configng/*.sh; do + mapfile -t temp_functionarray < <(grep -oP '^\w+::\w+' "$file") + functionarray+=("${temp_functionarray[@]}") + + mapfile -t temp_funnamearray < <(grep -oP '^\w+::\w+' "$file" | sed 's/.*:://') + funnamearray+=("${temp_funnamearray[@]}") + + mapfile -t temp_catagoryarray < <(grep -oP '^\w+::\w+' "$file" | sed 's/::.*//') + catagoryarray+=("${temp_catagoryarray[@]}") + + mapfile -t temp_descriptionarray < <(grep -oP '^# @description.*' "$file" | sed 's/^# @description //') + descriptionarray+=("${temp_descriptionarray[@]}") +done -readarray -t functionarray < <(grep -oP '^\w+::\w+' "$selfpath") -readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/.*:://') -readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/::.*//') -readarray -t descriptionarray < <(grep -oP '^# @description.*' "$selfpath" | sed 's/^# @description //') # test array #printf '%s\n' "${functionarray[@]}" @@ -42,7 +65,7 @@ readarray -t descriptionarray < <(grep -oP '^# @description.*' "$selfpath" | sed see_help(){ echo "" - echo "Usage: ${filename%.*} [ -h | -dev ]" + echo "Usage: ${filename%.*} [ -h | dev ]" echo -e "Options:" echo -e " -h Print this help." echo -e " dev Options:" @@ -52,7 +75,7 @@ see_help(){ } -# TEST 3 +# TEST 3 # check for -h -dev @ $1 # if -dev check @ $1 remove and shift $1 to check for x check_opts() { @@ -81,7 +104,7 @@ check_opts() { echo "Disabled durring current testing" else - see_help + see_help fi } diff --git a/bin/cpu_test.sh b/bin/cpu_test.sh deleted file mode 100755 index 2390654e5..000000000 --- a/bin/cpu_test.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related tests. -# - - -directory="$(dirname "$(readlink -f "$0")")" -filename=$(basename "${BASH_SOURCE[0]}") - -#libpath="$directory/../lib" -#selfpath="$libpath/configng/cpu.sh" - -if [[ -d "$directory/../lib" ]]; then - libpath="$directory"/../lib -elif [[ -d "/usr/lib/bash-utility" && -d "/usr/lib/configng" ]]; then - libpath="/usr/lib" -elif [[ -d "/../functions/bash-utility-master/src" ]] ; then - libpath="$directory"/../functions/bash-utility-master/src -else - echo "Libraries not found" - exit 0 -fi - -# Source the files relative to the script location -source "$libpath/bash-utility/string.sh" -source "$libpath/bash-utility/collection.sh" -source "$libpath/bash-utility/array.sh" -source "$libpath/bash-utility/check.sh" -source "$libpath/configng/cpu.sh" - -# Rest of the script... -# @description Print value from collection. -# -# @example -# collection::each "print_func" -# #Output -# Value in collection -print_func(){ - printf "%s\n" "$1" - return 0 - } - -# @description Check function exit code and exit script if != 0. -# -# @example -# check_return -# #Output -# Nothing -check_return(){ - if [ "$?" -ne 0 ]; then - exit 1 - fi - } - - -# Get policy -declare -i policy=$(cpu::get_policy) -printf 'Policy = %d\n' "$policy" -declare -i min_freq=$(cpu::get_min_freq $policy) -check_return -printf 'Minimum frequency = %d\n' "$min_freq" -declare -i max_freq=$(cpu::get_max_freq $policy) -check_return -printf 'Maximum frequency = %d\n' "$max_freq" -governor=$(cpu::get_governor $policy) -check_return -printf 'Governor = %s\n' "$governor" - -# Return frequencies as array -declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) -check_return -printf "\nAll frequencies\n" - -# Print all values in collection -printf "%s\n" "${freqs[@]}" | collection::each "print_func" -declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) -check_return -printf "\nAll governors\n" - - -# Print all values in collection -printf "%s\n" "${governors[@]}" | collection::each "print_func" - - -# Are we running as sudo? -[[ "$EUID" != 0 ]] && printf "Must call cpu::set_freq as sudo\n" && exit 1 - -# Before -printf "\nBefore\n" -cat /etc/default/cpufrequtils -cpu::set_freq $policy "$min_freq" "$max_freq" performance - -# After -printf "\nAfter\n" -cat /etc/default/cpufrequtils - diff --git a/bin/jampi-config b/bin/jampi-config new file mode 100644 index 000000000..4dd17d7c6 --- /dev/null +++ b/bin/jampi-config @@ -0,0 +1,212 @@ +#!/bin/bash + +# +# Copyright (c) 2023 Joseph C Turner +# All rights reserved. +# +# This script. +# demonstrates the compatibility of multiple interfaces for displaying menus or messages. +# It uses an array to set the options for all three menus (bash, whiptail, and dialog). +# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. +# If neither of these programs is available, it falls back to using bash. +# while both are installed falls back to whiptail to display the menu. +# The user can override the default program by passing an argument when running the script: +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + + + +#DIRECTORY variable to the absolute path of the script's directory +#directory=$(cd "$(dirname "$0")" && pwd) +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") +selfpath="$directory"/"$filename" +#libpath="${directory}"/"its-lib" #Include these scripts Library +libpath="$selfpath" + + +clear +# Check for the availability of whiptail and dialog command-line programs +# Set the default program to use for displaying messages +( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" +( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" +( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" + +# if both whiptail and dialog change to prefered +( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" + + +[[ "$1" == -b ]] && default="bash" +[[ "$1" == -w ]] && default="whiptail" +[[ "$1" == -n ]] && default="dialog" + +[[ "$1" == -m ]] && { +export NEWT_COLORS=" +root=blue,black +border=green,black +title=green,black +roottext=red,black +window=red,black +textbox=white,black +button=black,green +compactbutton=white,black +listbox=white,black +actlistbox=black,white +actsellistbox=black,green +checkbox=green,black +actcheckbox=black,green +" +} + +# Check the cpu architecture. for later handeling if nessery +architecture=$(dpkg --print-architecture) +[[ "$architecture" == "armf" ]] && true ; + +#setup menu arrays +readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) +readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) +readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) + +## System@Settings:Advanced Settings (armbian-config) +cmd_advance() +{ + sudo armbian-config +} + +## System@CPU info:Example from Bash Utility (cpu_test.sh) +cmd_cpu_info() +{ + [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; + [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +## System@CPU info:An example function (lscpu) +cmd_cpu_ls() +{ + + shell_command="$(lscpu)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Bootup Time:An example function (systemd-analyze time) +cmd_boot_time() +{ + + shell_command="$(systemd-analyze time)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Login Info :An example function (Login Info) +cmd_lslogins() +{ + + shell_command="$(lslogins)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +# Function to display a message using whiptail, dialog or printf depending on what is available on the system +see_message() +{ + # Use if neither whiptail nor dialog are available on the system + if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then + # Use printf to display the message + printf '%s ' "${shell_command[@]}" + + # Use as default if whiptail is available + elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then + # Use whiptail to display the message + whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext + + # Use if dialog is available on the system but not whiptail + elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then + # Use dialog to display the message + dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear + fi +} + +see_menu() +{ + + if [[ "$default" == "bash" ]]; then + PS3="Enter a number: " + select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done + + elif [[ "$default" == "whiptail" ]]; then + OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + + elif [[ "$default" == "dialog" ]]; then + OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + fi +} + +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " -b GNU bash " + echo -e " -n NCURSES dialog " + echo -e " -w NEWT whiptail - default colors " + echo -e " -m dark mode whiptail " + echo -e " -dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" + done + + } + +main() +{ + if [[ "$1" == --dev ]] ; then + default="bash" + local found=false + for i in "${!functionarray[@]}"; do + if [ "$2" == "${functionarray[i]}" ]; then + "${functionarray[i]}" + found=true + break + fi + done + if ! $found; then + see_help + exit 0 + fi + elif [[ "$1" == -h ]] ; then + see_help + else + see_menu + fi +} + +main "$@" diff --git a/lib/configng/cpu.sh b/lib/configng/cpu.sh index 772f9f4b1..e495e89f6 100644 --- a/lib/configng/cpu.sh +++ b/lib/configng/cpu.sh @@ -1,4 +1,4 @@ -#!/bin/bash + # # Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com # @@ -198,70 +198,3 @@ cpu::set_freq(){ return 0 } - -# @description SetUp Virtula spi MTD FLash, Remove spi MTD FLash. -# -# @example -# storage::set_spi_vflash s -# echo $? -# #Output -# -# -# @arg $1 int UnSet. -# @arg $1 int SetUp. -# -# @exitcode 0 If successful. -storage::set_spi_vflash(){ - # TODO handeling - [[ "$1" == "setup" ]] && create_virt_spi - [[ "$1" == "remove" ]] && remove_virt_spi - -} - -create_virt_spi() -{ - # Load the nandsim and mtdblock modules to create a virtual MTD device - - sudo modprobe mtdblock - #sudo modprobe nandsim - # Find the newly created MTD device - if [[ ! -e /dev/mtdblock0 ]]; then - sudo modprobe nandsim - irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') - else - echo "$( sudo ls /dev/mtdblock0 )" - fi - - # Create a symlink to the virtual MTD device with the name "spi0.0" - # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name - if [[ ! -e /dev/mtdblock0 ]]; then - ln -s /dev/$virtual_mtd /dev/mtdblock0 - fi - - # Create the mount point if it doesn't exist - mkdir -p /tmp/boot - - # Mount the virtual MTD device to the mount point - mount -t jffs2 /dev/mtdblock0 /tmp/boot - - # write a file to remove - touch /tmp/boot/Mounted_MTD.txt - - echo "$( sudo ls /dev/mtd* )" - -} - -remove_virt_spi() -{ - # Unmount the virtual MTD device from the mount point - umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') - - # Remove the symlink to the virtual MTD device - rm /dev/mtdblock0 - - # Unload the nandsim and mtdblock modules to remove the virtual MTD device - sudo modprobe -r mtdblock - sudo modprobe -r nandsim - - echo "0" -} diff --git a/lib/configng/storage.sh b/lib/configng/storage.sh new file mode 100644 index 000000000..272e13202 --- /dev/null +++ b/lib/configng/storage.sh @@ -0,0 +1,69 @@ + +# @description Remove simulated MTD spi flash. +# +# @example +# storage::set_spi_vflash +# echo $? +# #Output +# /dev/mtd0 +# /dev/mtd0ro +# /dev/mtdblock0 +# +# @exitcode 0 If successful. +storage::set_spi_vflash(){ + + # Load the nandsim and mtdblock modules to create a virtual MTD device + + sudo modprobe mtdblock + #sudo modprobe nandsim + # Find the newly created MTD device + if [[ ! -e /dev/mtdblock0 ]]; then + sudo modprobe nandsim + irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') + else + echo "$( sudo ls /dev/mtdblock0 )" + fi + + # Create a symlink to the virtual MTD device with the name "spi0.0" + # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name + if [[ ! -e /dev/mtdblock0 ]]; then + ln -s /dev/$virtual_mtd /dev/mtdblock0 + fi + + # Create the mount point if it doesn't exist + mkdir -p /tmp/boot + + # Mount the virtual MTD device to the mount point + mount -t jffs2 /dev/mtdblock0 /tmp/boot + + # write a file to remove + touch /tmp/boot/Mounted_MTD.txt + + echo "$( sudo ls /dev/mtd* )" + +} + + +# @description Remove simulated MTD spi flash. +# +# @example +# storage::rem_spi_vflash +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +storage::rem_spi_vflash(){ + + # Unmount the virtual MTD device from the mount point + umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') + + # Remove the symlink to the virtual MTD device + rm /dev/mtdblock0 + + # Unload the nandsim and mtdblock modules to remove the virtual MTD device + sudo modprobe -r mtdblock + sudo modprobe -r nandsim + + echo "0" +} From 07fc7b6dacabd29dec2b9e575c9050cf1e9948e8 Mon Sep 17 00:00:00 2001 From: tearran Date: Fri, 14 Jul 2023 22:35:07 -0700 Subject: [PATCH 05/48] new file: lib/configng/system.sh iterating sourcing libraies. migrated Run7ZipBenchmark() from armban monitor. added a basic api to call --- lib/configng/system.sh | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/configng/system.sh diff --git a/lib/configng/system.sh b/lib/configng/system.sh new file mode 100644 index 000000000..fa5b03f58 --- /dev/null +++ b/lib/configng/system.sh @@ -0,0 +1,37 @@ + + +# @description Return policy as int based on original armbian-config logic. +# +# @example +# system::see_7ZipBench +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +# +# @stdout tobd. +system::see_7ZipBench() { + echo -e "Preparing benchmark. Be patient please..." + # Do a quick 7-zip benchmark, check whether binary is there. If not install it + MyTool=$(which 7za || which 7zr) + [ -z "${MyTool}" ] && apt-get -f -qq -y install p7zip && MyTool=/usr/bin/7zr + [ -z "${MyTool}" ] && (echo "No 7-zip binary found and could not be installed. Aborting" >&2 ; exit 1) + # Send CLI monitoring to the background to be able to spot throttling and other problems + MonitoringOutput="$(mktemp /tmp/${0##*/}.XXXXXX)" + trap "rm \"${MonitoringOutput}\" ; exit 0" 0 1 2 3 15 + armbianmonitor -m >${MonitoringOutput} & + MonitoringPID=$! + # run 7-zip benchmarks after waiting 10 seconds to spot whether the system was idle before. + # We run the benchmark a single time by default unless otherwise specified on the command line + RunHowManyTimes=${runs:-1} + sleep 10 + for ((i=1;i<=RunHowManyTimes;i++)); do + "${MyTool}" b + done + # report CLI monitoring results as well + kill ${MonitoringPID} + echo -e "\nMonitoring output recorded while running the benchmark:\n" + sed -e '/^\s*$/d' -e '/^Stop/d' <${MonitoringOutput} + echo -e "\n" +} \ No newline at end of file From bca1200dd6ff6c07040e5b8a2f40518cb7f227e7 Mon Sep 17 00:00:00 2001 From: tearran Date: Sat, 15 Jul 2023 04:30:26 -0700 Subject: [PATCH 06/48] modified: bin/configng.sh modified: lib/configng/system.sh --- bin/configng.sh | 70 ++++++++++++++++++++++++++---------------- lib/configng/system.sh | 2 +- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/bin/configng.sh b/bin/configng.sh index 136a0409d..6726f29a5 100644 --- a/bin/configng.sh +++ b/bin/configng.sh @@ -1,4 +1,5 @@ #!/bin/bash + # # Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com # @@ -16,6 +17,7 @@ libpath="$directory/../lib" if [[ -d "$directory/../lib" ]]; then libpath="$directory"/../lib +# installed option todo change when lib location determined #elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then # libpath="/usr/lib" else @@ -23,13 +25,6 @@ else exit 0 fi -## Source the files relative to the script location -#source "$libpath/bash-utility/string.sh" -#source "$libpath/bash-utility/collection.sh" -#source "$libpath/bash-utility/array.sh" -#source "$libpath/bash-utility/check.sh" -#source "$libpath/configng/cpu.sh" - # Source the files relative to the script location for file in "$libpath"/bash-utility/*; do source "$file" @@ -57,29 +52,51 @@ for file in "$libpath"/configng/*.sh; do descriptionarray+=("${temp_descriptionarray[@]}") done + see_help_dev(){ + # Extract unique prefixes + declare -A prefixes + for i in "${!functionarray[@]}"; do + prefix="${functionarray[i]%%::*}" + prefixes["$prefix"]=1 + done -# test array -#printf '%s\n' "${functionarray[@]}" -#exit 0 - -see_help(){ + # Construct usage line + usage="" + for prefix in "${!prefixes[@]}"; do + usage+="[ $prefix::options ]" + done - echo "" - echo "Usage: ${filename%.*} [ -h | dev ]" - echo -e "Options:" - echo -e " -h Print this help." - echo -e " dev Options:" + +# echo "$usage" + echo "Usage: ${filename%.*} [ -h | foo ]" + echo "" + echo -e "Options:" + echo -e " -h) Print this help." + echo -e "" + echo -e " foo) Usage: ${filename%.*} foo $usage:: " + echo "" + + # Group options by prefix + declare -A groups for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" - done + prefix="${functionarray[i]%%::*}" + suffix="${functionarray[i]#*::}" + groups["$prefix"]+=$'\t\t'"$suffix\t${descriptionarray[i]}"$'\n' + done + + # Print grouped options + for group in "${!groups[@]}"; do + echo -e " $group::options" + echo -e "${groups[$group]}" + done +} - } -# TEST 3 +# TEST 7 # check for -h -dev @ $1 # if -dev check @ $1 remove and shift $1 to check for x check_opts() { - if [ "$1" == "dev" ]; then + if [ "$1" == "foo" ]; then shift # Shifts the arguments to the left, excluding the first argument ("-dev") function_name="$1" # Assigns the next argument as the function name shift # Shifts the arguments again to exclude the function name @@ -96,18 +113,19 @@ check_opts() { if [ "$found" == false ]; then echo "Invalid function name" + see_help_dev + exit 1 + fi - elif [[ "$1" == "dev" && "$2" == "cpu::set_freq" ]]; then + elif [[ "$1" == "foo" && "$2" == "cpu::set_freq" ]]; then # Disabled till understood. echo "cpu::set_freq policy min_freq max_freq performance" echo "Disabled durring current testing" else - see_help + see_help_dev fi } -#check_opts_test1 "$@" check_opts "$@" - diff --git a/lib/configng/system.sh b/lib/configng/system.sh index fa5b03f58..b5e3344e7 100644 --- a/lib/configng/system.sh +++ b/lib/configng/system.sh @@ -1,6 +1,6 @@ -# @description Return policy as int based on original armbian-config logic. +# @description 7-zip benchmark based on original armbianmonitor logic. # # @example # system::see_7ZipBench From 2bf9b19f32096fc3f2851a94b14ed8bbf3c20ad5 Mon Sep 17 00:00:00 2001 From: tearran Date: Sat, 15 Jul 2023 21:20:08 -0700 Subject: [PATCH 07/48] new file: lib/configng/benchymark.sh new file: lib/configng/board_led.sh modified: lib/configng/cpu.sh deleted: lib/configng/system.sh renamed: lib/configng/storage.sh -> lib/configng/vdrive.sh set funtion groups as files added change systems Board led behavor https://armbian.atlassian.net/browse/AR-449 tested arguments cpu, heartbeat, and none examples none, cpu, or pass arument some handeling. added banchymark group a migrated armbianmonitor function running armbianmonitor -m added system d boot time funtions. changed storage.sh catagory to vdrive.sh group --- lib/configng/benchymark.sh | 77 ++++++++++++++++++++++++++ lib/configng/board_led.sh | 63 +++++++++++++++++++++ lib/configng/cpu.sh | 13 ++--- lib/configng/system.sh | 37 ------------- lib/configng/{storage.sh => vdrive.sh} | 23 ++++++-- 5 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 lib/configng/benchymark.sh create mode 100644 lib/configng/board_led.sh delete mode 100644 lib/configng/system.sh rename lib/configng/{storage.sh => vdrive.sh} (68%) diff --git a/lib/configng/benchymark.sh b/lib/configng/benchymark.sh new file mode 100644 index 000000000..90a1ef118 --- /dev/null +++ b/lib/configng/benchymark.sh @@ -0,0 +1,77 @@ + +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Benchmark related functions. See +# https://systemd.io/ for more info. +# https://www.7-zip.org/ + +# @description system boot-up performance statistics. +# +# @example +# benchymark::see_systemd $1 (blame time chain) +# #Output +# time (quick check of boot time) +# balme (List modual load times) +# chain () +# +# @exitcode 0 If successful. +# +# @stdout tobd. +benchymark::see_monitor(){ + [[ $1 == "" ]] && clear && armbianmonitor -M ; + [[ $1 == $1 ]] && armbianmonitor "$1" ; + exit 0 + } +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Benchmark related functions. See +# https://systemd.io/ for more info. +# https://www.7-zip.org/ + +# @description system boot-up performance statistics. +# +# @example +# benchymark::see_systemd $1 (blame time chain) +# #Output +# time (quick check of boot time) +# balme (List modual load times) +# chain () +# +# @exitcode 0 If successful. +# +# @stdout tobd. +benchymark::see_boot_blame(){ + + [[ $1 == "blame" ]] && sys_blame=$( systemd-analyze blame ) ; + [[ $1 == "time" || $1 == "" ]] && sys_blame=$( systemd-analyze time ) ; + [[ $1 == "chain" ]] && sys_blame=$( systemd-analyze critical-chain ) ; + printf '%s\n' "${sys_blame[@]}" + exit 0 + } + + +# @description 7-zip benchmark based on original armbianmonitor logic. +# +# @example +# benchymark::see_7ZipBench +# echo $? +# #Output +# TBD +# +# @exitcode 0 If successful. +# +# @stdout tobd. +benchymark::see_7ZipBench() { + echo -e "Preparing benchmark. Be patient please..." + [[ $1 == "" ]] && armbianmonitor -z ; + } diff --git a/lib/configng/board_led.sh b/lib/configng/board_led.sh new file mode 100644 index 000000000..9eea83fa0 --- /dev/null +++ b/lib/configng/board_led.sh @@ -0,0 +1,63 @@ + +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# System boards led monitoring. See +# TBD + +# @description set the Sys board led to montor cpu activity. +# +# @example +# boardLED::set_sysled_cpu +# #Output +# Led blinks to set cpu +# +# @exitcode 0 If successful. +# +# @stdout cpu. +boardled::set_sysled_cpu(){ + + echo cpu | sudo tee /sys/class/leds/*/trigger + + } + +# @description set the Sys board led to montor none. +# +# @example +# boardLED::set_sysled_none +# #Output +# Led blinks to set no +# +# @exitcode 0 If successful. +# +# @stdout none. +boardled::set_sysled_none(){ + + echo none | sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger + } + +# @description See a list of board led options. +# +# @example +# boardLED::set_sysled_none +# #Output +# Led blinks to set no +# +# @exitcode 0 If successful. +# +# @stdout tbd. +boardled::see_sysled(){ + + # the avalible options + readarray triggers_led < <( cat /sys/class/leds/*/trigger ) + # see pass not argument the avalible options + [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; + # Set the systme Led blink to $1 valus + [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; + +} + diff --git a/lib/configng/cpu.sh b/lib/configng/cpu.sh index e495e89f6..6847be043 100644 --- a/lib/configng/cpu.sh +++ b/lib/configng/cpu.sh @@ -7,7 +7,6 @@ # warranty of any kind, whether express or implied. # # CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. -# # @description Return policy as int based on original armbian-config logic. # @@ -20,7 +19,7 @@ # @exitcode 0 If successful. # # @stdout Policy as integer. -cpu::get_policy(){ +cpu::see_policy(){ declare -i policy=0 [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 @@ -43,7 +42,7 @@ cpu::get_policy(){ # @exitcode 2 Function missing arguments. # # @stdout Space delimited string of CPU frequencies. -cpu::get_freqs(){ +cpu::see_freqs(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -69,7 +68,7 @@ cpu::get_freqs(){ # @exitcode 2 Function missing arguments. # # @stdout CPU minimum frequency as string. -cpu::get_min_freq(){ +cpu::see_min_freq(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -94,7 +93,7 @@ cpu::get_min_freq(){ # @exitcode 2 Function missing arguments. # # @stdout CPU maximum frequency as string. -cpu::get_max_freq(){ +cpu::see_max_freq(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -114,7 +113,7 @@ cpu::get_max_freq(){ # performance # # @arg $1 int policy. -cpu::get_governor(){ +cpu::see_governor(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -134,7 +133,7 @@ cpu::get_governor(){ # performance # # @arg $1 int policy. -cpu::get_governors(){ +cpu::see_governors(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value diff --git a/lib/configng/system.sh b/lib/configng/system.sh deleted file mode 100644 index b5e3344e7..000000000 --- a/lib/configng/system.sh +++ /dev/null @@ -1,37 +0,0 @@ - - -# @description 7-zip benchmark based on original armbianmonitor logic. -# -# @example -# system::see_7ZipBench -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -# -# @stdout tobd. -system::see_7ZipBench() { - echo -e "Preparing benchmark. Be patient please..." - # Do a quick 7-zip benchmark, check whether binary is there. If not install it - MyTool=$(which 7za || which 7zr) - [ -z "${MyTool}" ] && apt-get -f -qq -y install p7zip && MyTool=/usr/bin/7zr - [ -z "${MyTool}" ] && (echo "No 7-zip binary found and could not be installed. Aborting" >&2 ; exit 1) - # Send CLI monitoring to the background to be able to spot throttling and other problems - MonitoringOutput="$(mktemp /tmp/${0##*/}.XXXXXX)" - trap "rm \"${MonitoringOutput}\" ; exit 0" 0 1 2 3 15 - armbianmonitor -m >${MonitoringOutput} & - MonitoringPID=$! - # run 7-zip benchmarks after waiting 10 seconds to spot whether the system was idle before. - # We run the benchmark a single time by default unless otherwise specified on the command line - RunHowManyTimes=${runs:-1} - sleep 10 - for ((i=1;i<=RunHowManyTimes;i++)); do - "${MyTool}" b - done - # report CLI monitoring results as well - kill ${MonitoringPID} - echo -e "\nMonitoring output recorded while running the benchmark:\n" - sed -e '/^\s*$/d' -e '/^Stop/d' <${MonitoringOutput} - echo -e "\n" -} \ No newline at end of file diff --git a/lib/configng/storage.sh b/lib/configng/vdrive.sh similarity index 68% rename from lib/configng/storage.sh rename to lib/configng/vdrive.sh index 272e13202..771489f86 100644 --- a/lib/configng/storage.sh +++ b/lib/configng/vdrive.sh @@ -1,8 +1,19 @@ -# @description Remove simulated MTD spi flash. +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Externa Drive related functions. See +# http://linux-mtd.infradead.org/doc/general.html for more info. +# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC + +# @description Set up a simulated MTD spi flash for testing. # # @example -# storage::set_spi_vflash +# extra_drive::set_spi_vflash # echo $? # #Output # /dev/mtd0 @@ -10,7 +21,7 @@ # /dev/mtdblock0 # # @exitcode 0 If successful. -storage::set_spi_vflash(){ +extra_drive::set_spi_vflash(){ # Load the nandsim and mtdblock modules to create a virtual MTD device @@ -44,16 +55,16 @@ storage::set_spi_vflash(){ } -# @description Remove simulated MTD spi flash. +# @description Remove tsting simulated MTD spi flash. # # @example -# storage::rem_spi_vflash +# extra_drive::rem_spi_vflash # echo $? # #Output # 0 # # @exitcode 0 If successful. -storage::rem_spi_vflash(){ +extra_drive::rem_spi_vflash(){ # Unmount the virtual MTD device from the mount point umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') From 3e89f5ea1a22f75ccf831b9467361c2a04703d72 Mon Sep 17 00:00:00 2001 From: Joseph Turner Date: Sat, 15 Jul 2023 22:01:57 -0700 Subject: [PATCH 08/48] Update README.md --- README.md | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1653df4c9..325303253 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # configng This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility) -embedded in this project. This allows for functional programming in Bash and also modernizes -the monolithic nature of armbian-config. Error handling and validation are also included. -The idea is to provide an API in Bash that can be called from a TUI, GUI or CLI. Please -follow the coding standards which follow Bash Utility functions. - +embedded in this project. This allows for functional programming in Bash. Error handling and validation are also included. +The idea is to provide an API in Bash that can be called from a Command line interface, Text User interface and others. Why Bash? Well, because it's going to be in every distribution. Striped down distributions may not include Python, C/C++, etc. build/runtime environments @@ -12,9 +9,41 @@ may not include Python, C/C++, etc. build/runtime environments * `sudo apt install git` * `cd ~/` * `git clone https://github.com/armbian/configng.git` -* `cd ~/configng/test` -* `sudo ./cpu_test.sh` -If all goes well you should see all the functions in cpu.sh called and output diaplayed. +* `sudo ~/configng/configng.sh` + +#### If all goes well you should see list or avalible commands +``` +Usage: configng [ -h | foo ] + +Options: + -h) Print this help. + + foo) Usage: configng foo [ boardled::options ][ cpu::options ][ extra_drive::options ][ benchymark::options ]:: + + boardled::options + set_sysled_cpu set the Sys board led to montor cpu activity. + set_sysled_none set the Sys board led to montor none. + see_sysled See a list of board led options. + + cpu::options + see_policy Return policy as int based on original armbian-config logic. + see_freqs Return CPU frequencies as string delimited by space. + see_min_freq Return CPU minimum frequency as string. + see_max_freq Return CPU maximum frequency as string. + see_governor Return CPU governor as string. + see_governors Return CPU governors as string delimited by space. + set_freq Set min, max and CPU governor. + + extra_drive::options + set_spi_vflash Set up a simulated MTD spi flash for testing. + rem_spi_vflash Remove tsting simulated MTD spi flash. + + benchymark::options + see_monitor system boot-up performance statistics. + see_boot_blame system boot-up performance statistics. + see_7ZipBench 7-zip benchmark based on original armbianmonitor logic. + +``` ## Coding standards [Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, @@ -40,7 +69,7 @@ printf '%s\n' "${1##$2}" } ``` -Functions should follow filename::func_name style. Then you can tell just from the name which +Functions should follow ~~filename~~::func_name style. Then you can tell just from the name which file the function is located in. Return codes should also follow a similar pattern: * 0 Successful * 1 Not found From 47a549fa32db032269f46435f55004dd67456e33 Mon Sep 17 00:00:00 2001 From: tearran Date: Wed, 12 Jul 2023 08:36:48 -0700 Subject: [PATCH 09/48] Setup for root copy --- .gitignore | 2 - bin/cli_options.sh | 90 + bin/cpu_test.sh | 214 ++ bin/jampi-config | 212 ++ functions/bash-utility-master/.editorconfig | 22 - functions/bash-utility-master/.gitignore | 3 - functions/bash-utility-master/.remarkrc | 8 - .../src => lib/bash-utility}/array.sh | 0 .../src => lib/bash-utility}/check.sh | 0 .../src => lib/bash-utility}/collection.sh | 0 .../src => lib/bash-utility}/date.sh | 0 .../src => lib/bash-utility}/debug.sh | 0 .../src => lib/bash-utility}/file.sh | 0 .../src => lib/bash-utility}/format.sh | 0 .../src => lib/bash-utility}/interaction.sh | 0 .../src => lib/bash-utility}/json.sh | 0 .../src => lib/bash-utility}/misc.sh | 0 .../src => lib/bash-utility}/os.sh | 0 .../src => lib/bash-utility}/string.sh | 0 .../src => lib/bash-utility}/terminal.sh | 0 .../src => lib/bash-utility}/validation.sh | 0 .../src => lib/bash-utility}/variable.sh | 0 lib/configng/cpu.sh | 267 ++ .../bash-utility}/CODE_OF_CONDUCT.md | 0 .../bash-utility}/CONTRIBUTING.md | 0 .../bash-utility}/LICENSE | 0 .../bash-utility}/README.md | 0 .../bash-utility}/bash_utility.sh | 0 .../bash-utility}/bin/bashdoc.awk | 0 .../bash-utility}/bin/generate_readme.sh | 0 .../bash-utility}/image/bash-utility.png | Bin .../bash-utility}/image/logo.png | Bin src/bash-utility/src/array.sh | 284 ++ src/bash-utility/src/check.sh | 34 + src/bash-utility/src/collection.sh | 287 ++ src/bash-utility/src/date.sh | 744 ++++ src/bash-utility/src/debug.sh | 49 + src/bash-utility/src/file.sh | 222 ++ src/bash-utility/src/format.sh | 183 + src/bash-utility/src/interaction.sh | 96 + src/bash-utility/src/json.sh | 25 + src/bash-utility/src/misc.sh | 121 + src/bash-utility/src/os.sh | 135 + src/bash-utility/src/string.sh | 198 ++ src/bash-utility/src/terminal.sh | 51 + src/bash-utility/src/validation.sh | 244 ++ src/bash-utility/src/variable.sh | 144 + src/configng/README.md | 62 + .../bash-utility-master/CODE_OF_CONDUCT.md | 76 + .../bash-utility-master/CONTRIBUTING.md | 129 + .../functions/bash-utility-master/LICENSE | 21 + .../functions/bash-utility-master/README.md | 3026 +++++++++++++++++ .../bash-utility-master/bash_utility.sh | 20 + .../bash-utility-master/bin/bashdoc.awk | 275 ++ .../bin/generate_readme.sh | 400 +++ .../image/bash-utility.png | Bin 0 -> 32396 bytes .../bash-utility-master/image/logo.png | Bin 0 -> 23716 bytes .../bash-utility-master/src/array.sh | 284 ++ .../bash-utility-master/src/check.sh | 34 + .../bash-utility-master/src/collection.sh | 287 ++ .../functions/bash-utility-master/src/date.sh | 744 ++++ .../bash-utility-master/src/debug.sh | 49 + .../functions/bash-utility-master/src/file.sh | 222 ++ .../bash-utility-master/src/format.sh | 183 + .../bash-utility-master/src/interaction.sh | 96 + .../functions/bash-utility-master/src/json.sh | 25 + .../functions/bash-utility-master/src/misc.sh | 121 + .../functions/bash-utility-master/src/os.sh | 168 + .../bash-utility-master/src/string.sh | 198 ++ .../bash-utility-master/src/terminal.sh | 51 + .../bash-utility-master/src/validation.sh | 244 ++ .../bash-utility-master/src/variable.sh | 144 + {functions => src/configng/functions}/cpu.sh | 0 {test => src/configng/test}/cpu_test.sh | 0 74 files changed, 10459 insertions(+), 35 deletions(-) delete mode 100644 .gitignore create mode 100644 bin/cli_options.sh create mode 100755 bin/cpu_test.sh create mode 100755 bin/jampi-config delete mode 100644 functions/bash-utility-master/.editorconfig delete mode 100644 functions/bash-utility-master/.gitignore delete mode 100644 functions/bash-utility-master/.remarkrc rename {functions/bash-utility-master/src => lib/bash-utility}/array.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/check.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/collection.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/date.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/debug.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/file.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/format.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/interaction.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/json.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/misc.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/os.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/string.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/terminal.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/validation.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master/src => lib/bash-utility}/variable.sh (100%) mode change 100755 => 100644 create mode 100644 lib/configng/cpu.sh rename {functions/bash-utility-master => src/bash-utility}/CODE_OF_CONDUCT.md (100%) rename {functions/bash-utility-master => src/bash-utility}/CONTRIBUTING.md (100%) rename {functions/bash-utility-master => src/bash-utility}/LICENSE (100%) rename {functions/bash-utility-master => src/bash-utility}/README.md (100%) rename {functions/bash-utility-master => src/bash-utility}/bash_utility.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master => src/bash-utility}/bin/bashdoc.awk (100%) mode change 100755 => 100644 rename {functions/bash-utility-master => src/bash-utility}/bin/generate_readme.sh (100%) mode change 100755 => 100644 rename {functions/bash-utility-master => src/bash-utility}/image/bash-utility.png (100%) rename {functions/bash-utility-master => src/bash-utility}/image/logo.png (100%) create mode 100644 src/bash-utility/src/array.sh create mode 100644 src/bash-utility/src/check.sh create mode 100644 src/bash-utility/src/collection.sh create mode 100644 src/bash-utility/src/date.sh create mode 100644 src/bash-utility/src/debug.sh create mode 100644 src/bash-utility/src/file.sh create mode 100644 src/bash-utility/src/format.sh create mode 100644 src/bash-utility/src/interaction.sh create mode 100644 src/bash-utility/src/json.sh create mode 100644 src/bash-utility/src/misc.sh create mode 100644 src/bash-utility/src/os.sh create mode 100644 src/bash-utility/src/string.sh create mode 100644 src/bash-utility/src/terminal.sh create mode 100644 src/bash-utility/src/validation.sh create mode 100644 src/bash-utility/src/variable.sh create mode 100644 src/configng/README.md create mode 100644 src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md create mode 100644 src/configng/functions/bash-utility-master/CONTRIBUTING.md create mode 100644 src/configng/functions/bash-utility-master/LICENSE create mode 100644 src/configng/functions/bash-utility-master/README.md create mode 100644 src/configng/functions/bash-utility-master/bash_utility.sh create mode 100644 src/configng/functions/bash-utility-master/bin/bashdoc.awk create mode 100644 src/configng/functions/bash-utility-master/bin/generate_readme.sh create mode 100644 src/configng/functions/bash-utility-master/image/bash-utility.png create mode 100644 src/configng/functions/bash-utility-master/image/logo.png create mode 100644 src/configng/functions/bash-utility-master/src/array.sh create mode 100644 src/configng/functions/bash-utility-master/src/check.sh create mode 100644 src/configng/functions/bash-utility-master/src/collection.sh create mode 100644 src/configng/functions/bash-utility-master/src/date.sh create mode 100644 src/configng/functions/bash-utility-master/src/debug.sh create mode 100644 src/configng/functions/bash-utility-master/src/file.sh create mode 100644 src/configng/functions/bash-utility-master/src/format.sh create mode 100644 src/configng/functions/bash-utility-master/src/interaction.sh create mode 100644 src/configng/functions/bash-utility-master/src/json.sh create mode 100644 src/configng/functions/bash-utility-master/src/misc.sh create mode 100644 src/configng/functions/bash-utility-master/src/os.sh create mode 100644 src/configng/functions/bash-utility-master/src/string.sh create mode 100644 src/configng/functions/bash-utility-master/src/terminal.sh create mode 100644 src/configng/functions/bash-utility-master/src/validation.sh create mode 100644 src/configng/functions/bash-utility-master/src/variable.sh rename {functions => src/configng/functions}/cpu.sh (100%) rename {test => src/configng/test}/cpu_test.sh (100%) mode change 100755 => 100644 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 58c32c89c..000000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/.project -/.settings/ diff --git a/bin/cli_options.sh b/bin/cli_options.sh new file mode 100644 index 000000000..0062725b6 --- /dev/null +++ b/bin/cli_options.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# + +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") + +libpath="$directory/../lib" +selfpath="$libpath/configng/cpu.sh" + +if [[ -d "$directory/../lib" ]]; then + libpath="$directory"/../lib +elif [[ -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then + libpath="/usr/lib" +else + echo "Libraries not found" + exit 0 +fi + +# Source the files relative to the script location +source "$libpath/bash-utility/string.sh" +source "$libpath/bash-utility/collection.sh" +source "$libpath/bash-utility/array.sh" +source "$libpath/bash-utility/check.sh" +source "$libpath/configng/cpu.sh" + +readarray -t functionarray < <(grep -oP '^\w+::\w+' "$selfpath") +readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/.*:://') +readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/::.*//') +readarray -t descriptionarray < <(grep -oP '^# @description.*' "$selfpath" | sed 's/^# @description //') + +# test array +#printf '%s\n' "${functionarray[@]}" +#exit 0 + +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -dev ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" + done + + } + +# TEST 3 +# check for -h -dev @ $1 +# if -dev check @ $1 remove and shift $1 to check for x +check_opts() { + if [ "$1" == "dev" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-dev") + function_name="$1" # Assigns the next argument as the function name + shift # Shifts the arguments again to exclude the function name + + found=false + + for ((i=0; i<${#functionarray[@]}; i++)); do + if [ "$function_name" == "${functionarray[i]}" ]; then + found=true + ${functionarray[i]} "$@" + break + fi + done + + if [ "$found" == false ]; then + echo "Invalid function name" + fi + + elif [[ "$1" == "dev" && "$2" == "cpu::set_freq" ]]; then + # Disabled till understood. + echo "cpu::set_freq policy min_freq max_freq performance" + echo "Disabled durring current testing" + + else + see_help + fi +} + +#check_opts_test1 "$@" +check_opts "$@" + diff --git a/bin/cpu_test.sh b/bin/cpu_test.sh new file mode 100755 index 000000000..9a50d4954 --- /dev/null +++ b/bin/cpu_test.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# CPU related tests. +# + + +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") + +#libpath="$directory/../lib" +#selfpath="$libpath/configng/cpu.sh" + +if [[ -d "$directory/../lib" ]]; then + libpath="$directory"/../lib +elif [[ -d "/usr/lib/bash-utility" && -d "/usr/lib/configng" ]]; then + libpath="/usr/lib" +elif [[ -d "/../functions/bash-utility-master/src" ]] ; then + libpath="$directory"/../functions/bash-utility-master/src +else + echo "Libraries not found" + exit 0 +fi + +# Source the files relative to the script location +source "$libpath/bash-utility/string.sh" +source "$libpath/bash-utility/collection.sh" +source "$libpath/bash-utility/array.sh" +source "$libpath/bash-utility/check.sh" +source "$libpath/configng/cpu.sh" + +# Rest of the script... +# @description Print value from collection. +# +# @example +# collection::each "print_func" +# #Output +# Value in collection +print_func(){ + printf "%s\n" "$1" + return 0 + } + +# @description Check function exit code and exit script if != 0. +# +# @example +# check_return +# #Output +# Nothing +check_return(){ + if [ "$?" -ne 0 ]; then + exit 1 + fi + } + +see_cpu(){ +# Get policy +declare -i policy=$(cpu::get_policy) +printf 'Policy = %d\n' "$policy" +declare -i min_freq=$(cpu::get_min_freq $policy) +check_return +printf 'Minimum frequency = %d\n' "$min_freq" +declare -i max_freq=$(cpu::get_max_freq $policy) +check_return +printf 'Maximum frequency = %d\n' "$max_freq" +governor=$(cpu::get_governor $policy) +check_return +printf 'Governor = %s\n' "$governor" + +# Return frequencies as array +declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) +check_return +printf "\nAll frequencies\n" + +# Print all values in collection +printf "%s\n" "${freqs[@]}" | collection::each "print_func" +declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) +check_return +printf "\nAll governors\n" + + +# Print all values in collection +printf "%s\n" "${governors[@]}" | collection::each "print_func" + + +# Are we running as sudo? +[[ "$EUID" != 0 ]] && printf "Must call cpu::set_freq as sudo\n" && exit 1 + +# Before +printf "\nBefore\n" +cat /etc/default/cpufrequtils +cpu::set_freq $policy "$min_freq" "$max_freq" performance + +# After +printf "\nAfter\n" +cat /etc/default/cpufrequtils + +} + +readarray -t functionarray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh") +readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh" | sed 's/.*:://') +readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$libpat/configng/cpu.sh" | sed 's/::.*//') +readarray -t descriptionarray < <(grep -oP '^# @description.*' "$libpath/configng/cpu.sh" | sed 's/^# @description //') + +#printf '%s\n' "${functionarray[@]}" +#exit 0 +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -dev | ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " -dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" + done + echo -e " -cpu Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s\n' "${funnamearray[i]}" "${descriptionarray[i]}" + done + + } +# check for -dev -h options +check_opts_test1() +{ + if [[ "$1" == -dev ]] ; then + default="bash" + local found=false + for i in "${!functionarray[@]}"; do + if [ "$2" == "${functionarray[i]}" ]; then + "${functionarray[i]}" + found=true + break + fi + done + if ! $found; then + see_help + exit 0 + fi + elif [[ "$1" == "-cpu" ]] ; then + echo -e " " + echo -e " TODO:" + for i in "${!functionarray[@]}"; do + + printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" + done + + elif [[ "$1" == -h ]] ; then + see_help + else + see_cpu + fi +} + +# check for -h -dev +# if -dev check for number +check_opts_test2(){ + +if [ "$1" == "-dev" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-dev") + function_name="$1" # Assigns the next argument as the function name + shift # Shifts the arguments again to exclude the function name + + case "$function_name" in + 0) echo "Calling function 0 with arguments: $@" ;; + 1) echo "Calling function 1 with arguments: $@" ;; + 2) echo "Calling function 2 with arguments: $@" ;; + 3) echo "Calling function 3 with arguments: $@" ;; + 4) echo "Calling function 4 with arguments: $@" ;; + *) echo "Invalid function name" ;; + esac +else + echo "No -dev flag found" +fi + +} +# check for -h -dev @ $1 +# if -dev check @ $1 remove and shift $1 to check for x +check_opts() { + if [ "$1" == "-dev" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-dev") + function_name="$1" # Assigns the next argument as the function name + shift # Shifts the arguments again to exclude the function name + + found=false + + for ((i=0; i<${#functionarray[@]}; i++)); do + if [ "$function_name" == "${functionarray[i]}" ]; then + found=true + ${functionarray[i]} "$@" + break + fi + done + + if [ "$found" == false ]; then + echo "Invalid function name" + fi + + elif [ "$1" == "-h" ]; then + see_help + else + see_cpu + fi +} + +#check_opts_test1 "$@" +check_opts "$@" + +#cpu::set_freq $policy "$min_freq" "$max_freq" performance diff --git a/bin/jampi-config b/bin/jampi-config new file mode 100755 index 000000000..4dd17d7c6 --- /dev/null +++ b/bin/jampi-config @@ -0,0 +1,212 @@ +#!/bin/bash + +# +# Copyright (c) 2023 Joseph C Turner +# All rights reserved. +# +# This script. +# demonstrates the compatibility of multiple interfaces for displaying menus or messages. +# It uses an array to set the options for all three menus (bash, whiptail, and dialog). +# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. +# If neither of these programs is available, it falls back to using bash. +# while both are installed falls back to whiptail to display the menu. +# The user can override the default program by passing an argument when running the script: +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + + + +#DIRECTORY variable to the absolute path of the script's directory +#directory=$(cd "$(dirname "$0")" && pwd) +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") +selfpath="$directory"/"$filename" +#libpath="${directory}"/"its-lib" #Include these scripts Library +libpath="$selfpath" + + +clear +# Check for the availability of whiptail and dialog command-line programs +# Set the default program to use for displaying messages +( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" +( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" +( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" + +# if both whiptail and dialog change to prefered +( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" + + +[[ "$1" == -b ]] && default="bash" +[[ "$1" == -w ]] && default="whiptail" +[[ "$1" == -n ]] && default="dialog" + +[[ "$1" == -m ]] && { +export NEWT_COLORS=" +root=blue,black +border=green,black +title=green,black +roottext=red,black +window=red,black +textbox=white,black +button=black,green +compactbutton=white,black +listbox=white,black +actlistbox=black,white +actsellistbox=black,green +checkbox=green,black +actcheckbox=black,green +" +} + +# Check the cpu architecture. for later handeling if nessery +architecture=$(dpkg --print-architecture) +[[ "$architecture" == "armf" ]] && true ; + +#setup menu arrays +readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) +readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) +readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) + +## System@Settings:Advanced Settings (armbian-config) +cmd_advance() +{ + sudo armbian-config +} + +## System@CPU info:Example from Bash Utility (cpu_test.sh) +cmd_cpu_info() +{ + [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; + [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +## System@CPU info:An example function (lscpu) +cmd_cpu_ls() +{ + + shell_command="$(lscpu)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Bootup Time:An example function (systemd-analyze time) +cmd_boot_time() +{ + + shell_command="$(systemd-analyze time)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Login Info :An example function (Login Info) +cmd_lslogins() +{ + + shell_command="$(lslogins)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +# Function to display a message using whiptail, dialog or printf depending on what is available on the system +see_message() +{ + # Use if neither whiptail nor dialog are available on the system + if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then + # Use printf to display the message + printf '%s ' "${shell_command[@]}" + + # Use as default if whiptail is available + elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then + # Use whiptail to display the message + whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext + + # Use if dialog is available on the system but not whiptail + elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then + # Use dialog to display the message + dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear + fi +} + +see_menu() +{ + + if [[ "$default" == "bash" ]]; then + PS3="Enter a number: " + select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done + + elif [[ "$default" == "whiptail" ]]; then + OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + + elif [[ "$default" == "dialog" ]]; then + OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + fi +} + +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " -b GNU bash " + echo -e " -n NCURSES dialog " + echo -e " -w NEWT whiptail - default colors " + echo -e " -m dark mode whiptail " + echo -e " -dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" + done + + } + +main() +{ + if [[ "$1" == --dev ]] ; then + default="bash" + local found=false + for i in "${!functionarray[@]}"; do + if [ "$2" == "${functionarray[i]}" ]; then + "${functionarray[i]}" + found=true + break + fi + done + if ! $found; then + see_help + exit 0 + fi + elif [[ "$1" == -h ]] ; then + see_help + else + see_menu + fi +} + +main "$@" diff --git a/functions/bash-utility-master/.editorconfig b/functions/bash-utility-master/.editorconfig deleted file mode 100644 index f9b075876..000000000 --- a/functions/bash-utility-master/.editorconfig +++ /dev/null @@ -1,22 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -# Unix-style newlines with a newline ending every file -[*] -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -# for shfmt -[*.sh] -indent_style = space -indent_size = 4 -shell_variant = bash -switch_case_indent = true -space_redirects = true - -[*.md] -trim_trailing_whitespace = false \ No newline at end of file diff --git a/functions/bash-utility-master/.gitignore b/functions/bash-utility-master/.gitignore deleted file mode 100644 index 7c7bf7091..000000000 --- a/functions/bash-utility-master/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/tmp -gh-pages -hugo-docs diff --git a/functions/bash-utility-master/.remarkrc b/functions/bash-utility-master/.remarkrc deleted file mode 100644 index a3ff1e10d..000000000 --- a/functions/bash-utility-master/.remarkrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "plugins": [ - "remark-preset-lint-markdown-style-guide", - ["remark-lint-list-item-spacing", false], - ["remark-lint-maximum-line-length", false], - ["remark-lint-no-duplicate-headings", false] - ] -} diff --git a/functions/bash-utility-master/src/array.sh b/lib/bash-utility/array.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/array.sh rename to lib/bash-utility/array.sh diff --git a/functions/bash-utility-master/src/check.sh b/lib/bash-utility/check.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/check.sh rename to lib/bash-utility/check.sh diff --git a/functions/bash-utility-master/src/collection.sh b/lib/bash-utility/collection.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/collection.sh rename to lib/bash-utility/collection.sh diff --git a/functions/bash-utility-master/src/date.sh b/lib/bash-utility/date.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/date.sh rename to lib/bash-utility/date.sh diff --git a/functions/bash-utility-master/src/debug.sh b/lib/bash-utility/debug.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/debug.sh rename to lib/bash-utility/debug.sh diff --git a/functions/bash-utility-master/src/file.sh b/lib/bash-utility/file.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/file.sh rename to lib/bash-utility/file.sh diff --git a/functions/bash-utility-master/src/format.sh b/lib/bash-utility/format.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/format.sh rename to lib/bash-utility/format.sh diff --git a/functions/bash-utility-master/src/interaction.sh b/lib/bash-utility/interaction.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/interaction.sh rename to lib/bash-utility/interaction.sh diff --git a/functions/bash-utility-master/src/json.sh b/lib/bash-utility/json.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/json.sh rename to lib/bash-utility/json.sh diff --git a/functions/bash-utility-master/src/misc.sh b/lib/bash-utility/misc.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/misc.sh rename to lib/bash-utility/misc.sh diff --git a/functions/bash-utility-master/src/os.sh b/lib/bash-utility/os.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/os.sh rename to lib/bash-utility/os.sh diff --git a/functions/bash-utility-master/src/string.sh b/lib/bash-utility/string.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/string.sh rename to lib/bash-utility/string.sh diff --git a/functions/bash-utility-master/src/terminal.sh b/lib/bash-utility/terminal.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/terminal.sh rename to lib/bash-utility/terminal.sh diff --git a/functions/bash-utility-master/src/validation.sh b/lib/bash-utility/validation.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/validation.sh rename to lib/bash-utility/validation.sh diff --git a/functions/bash-utility-master/src/variable.sh b/lib/bash-utility/variable.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/src/variable.sh rename to lib/bash-utility/variable.sh diff --git a/lib/configng/cpu.sh b/lib/configng/cpu.sh new file mode 100644 index 000000000..772f9f4b1 --- /dev/null +++ b/lib/configng/cpu.sh @@ -0,0 +1,267 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. +# + +# @description Return policy as int based on original armbian-config logic. +# +# @example +# cpu::get_policy +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +# +# @stdout Policy as integer. +cpu::get_policy(){ + declare -i policy=0 + [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 + [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 + [[ -d /sys/devices/system/cpu/cpufreq/policy0 && -d /sys/devices/system/cpu/cpufreq/policy2 ]] && policy=2 + printf '%d\n' "$policy" +} + +# @description Return CPU frequencies as string delimited by space. +# +# @example +# cpu::get_freqs 0 +# echo $? +# #Output +# 648000 816000 912000 960000 1008000 1056000 1104000 1152000 +# +# @arg $1 int policy. +# +# @exitcode 0 If successful. +# @exitcode 1 If file not found. +# @exitcode 2 Function missing arguments. +# +# @stdout Space delimited string of CPU frequencies. +cpu::get_freqs(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_frequencies" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU minimum frequency as string. +# +# @example +# cpu::get_min_freq 0 +# echo $? +# #Output +# 648000 +# +# @arg $1 int policy. +# +# @exitcode 0 If successful. +# @exitcode 1 If file not found. +# @exitcode 2 Function missing arguments. +# +# @stdout CPU minimum frequency as string. +cpu::get_min_freq(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_min_freq" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU maximum frequency as string. +# +# @example +# cpu::get_max_freq 0 +# echo $? +# #Output +# 1152000 +# +# @arg $1 int policy. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout CPU maximum frequency as string. +cpu::get_max_freq(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_max_freq" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU governor as string. +# +# @example +# cpu::get_governor 0 +# echo $? +# #Output +# performance +# +# @arg $1 int policy. +cpu::get_governor(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_governor" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Return CPU governors as string delimited by space. +# +# @example +# cpu::get_governors 0 +# echo $? +# #Output +# performance +# +# @arg $1 int policy. +cpu::get_governors(){ + # Check number of arguments + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_governors" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + # Return value + printf '%s\n' "$(cat $file)" +} + +# @description Set min, max and CPU governor. +# +# @example +# cpu::set_freq 0 648000 1152000 performance +# echo $? +# #Output +# performance +# +# @arg $1 int Policy. +# @arg $2 int Minimum frequency. +# @arg $3 int Maximum frequency. +# @arg $4 string Governor. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode 3 Invalid minimum frequency. +# @exitcode 4 Invalid maximum frequency. +# @exitcode 5 Minimum frequency must be <= maximum frequency. +# @exitcode 6 Invalid governor. +cpu::set_freq(){ + # Check number of arguments + [[ $# -lt 4 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Build file based on policy value + local file="/etc/default/cpufrequtils" + # Check if file exists + [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 + declare -i policy=$1 + declare -i min_freq=$2 + declare -i max_freq=$3 + local governor=$4 + # Return frequencies as array + declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) + # Validate minimum frequency + array::contains "$min_freq" ${freqs[@]} + [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 3 + # Validate maximum frequency + array::contains "$max_freq" ${freqs[@]} + [[ $? != 0 ]] && printf "%s: Invalid maximum frequency\n" "${FUNCNAME[0]}" && return 4 + # Validate minimum frequency is <= maximum frequency + [ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 + # Return governors + declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) + # Validate maximum governor + array::contains "$governor" ${governor[@]} + [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 6 + # Update file + sed -i "s/MIN_SPEED=.*/MIN_SPEED=$min_freq/" "$file" + sed -i "s/MAX_SPEED=.*/MAX_SPEED=$max_freq/" "$file" + sed -i "s/GOVERNOR=.*/GOVERNOR=$governor/" "$file" + # Return value + return 0 +} + + +# @description SetUp Virtula spi MTD FLash, Remove spi MTD FLash. +# +# @example +# storage::set_spi_vflash s +# echo $? +# #Output +# +# +# @arg $1 int UnSet. +# @arg $1 int SetUp. +# +# @exitcode 0 If successful. +storage::set_spi_vflash(){ + # TODO handeling + [[ "$1" == "setup" ]] && create_virt_spi + [[ "$1" == "remove" ]] && remove_virt_spi + +} + +create_virt_spi() +{ + # Load the nandsim and mtdblock modules to create a virtual MTD device + + sudo modprobe mtdblock + #sudo modprobe nandsim + # Find the newly created MTD device + if [[ ! -e /dev/mtdblock0 ]]; then + sudo modprobe nandsim + irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') + else + echo "$( sudo ls /dev/mtdblock0 )" + fi + + # Create a symlink to the virtual MTD device with the name "spi0.0" + # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name + if [[ ! -e /dev/mtdblock0 ]]; then + ln -s /dev/$virtual_mtd /dev/mtdblock0 + fi + + # Create the mount point if it doesn't exist + mkdir -p /tmp/boot + + # Mount the virtual MTD device to the mount point + mount -t jffs2 /dev/mtdblock0 /tmp/boot + + # write a file to remove + touch /tmp/boot/Mounted_MTD.txt + + echo "$( sudo ls /dev/mtd* )" + +} + +remove_virt_spi() +{ + # Unmount the virtual MTD device from the mount point + umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') + + # Remove the symlink to the virtual MTD device + rm /dev/mtdblock0 + + # Unload the nandsim and mtdblock modules to remove the virtual MTD device + sudo modprobe -r mtdblock + sudo modprobe -r nandsim + + echo "0" +} diff --git a/functions/bash-utility-master/CODE_OF_CONDUCT.md b/src/bash-utility/CODE_OF_CONDUCT.md similarity index 100% rename from functions/bash-utility-master/CODE_OF_CONDUCT.md rename to src/bash-utility/CODE_OF_CONDUCT.md diff --git a/functions/bash-utility-master/CONTRIBUTING.md b/src/bash-utility/CONTRIBUTING.md similarity index 100% rename from functions/bash-utility-master/CONTRIBUTING.md rename to src/bash-utility/CONTRIBUTING.md diff --git a/functions/bash-utility-master/LICENSE b/src/bash-utility/LICENSE similarity index 100% rename from functions/bash-utility-master/LICENSE rename to src/bash-utility/LICENSE diff --git a/functions/bash-utility-master/README.md b/src/bash-utility/README.md similarity index 100% rename from functions/bash-utility-master/README.md rename to src/bash-utility/README.md diff --git a/functions/bash-utility-master/bash_utility.sh b/src/bash-utility/bash_utility.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/bash_utility.sh rename to src/bash-utility/bash_utility.sh diff --git a/functions/bash-utility-master/bin/bashdoc.awk b/src/bash-utility/bin/bashdoc.awk old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/bin/bashdoc.awk rename to src/bash-utility/bin/bashdoc.awk diff --git a/functions/bash-utility-master/bin/generate_readme.sh b/src/bash-utility/bin/generate_readme.sh old mode 100755 new mode 100644 similarity index 100% rename from functions/bash-utility-master/bin/generate_readme.sh rename to src/bash-utility/bin/generate_readme.sh diff --git a/functions/bash-utility-master/image/bash-utility.png b/src/bash-utility/image/bash-utility.png similarity index 100% rename from functions/bash-utility-master/image/bash-utility.png rename to src/bash-utility/image/bash-utility.png diff --git a/functions/bash-utility-master/image/logo.png b/src/bash-utility/image/logo.png similarity index 100% rename from functions/bash-utility-master/image/logo.png rename to src/bash-utility/image/logo.png diff --git a/src/bash-utility/src/array.sh b/src/bash-utility/src/array.sh new file mode 100644 index 000000000..42d5883b8 --- /dev/null +++ b/src/bash-utility/src/array.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash + +# @file Array +# @brief Functions for array operations and manipulations. + +# @description Check if item exists in the given array. +# +# @example +# array=("a" "b" "c") +# array::contains "c" ${array[@]} +# #Output +# 0 +# +# @arg $1 mixed Item to search (needle). +# @arg $2 array array to be searched (haystack). +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found in the array. +# @exitcode 2 Function missing arguments. +array::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare query="${1:-}" + shift + + for element in "${@}"; do + [[ "${element}" == "${query}" ]] && return 0 + done + + return 1 +} + +# @description Remove duplicate items from the array. +# +# @example +# array=("a" "b" "a" "c") +# printf "%s" "$(array::dedupe ${array[@]})" +# #Output +# a +# b +# c +# +# @arg $1 array Array to be deduped. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Deduplicated array. +array::dedupe() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -A arr_tmp + declare -a arr_unique + for i in "$@"; do + { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue + arr_unique+=("${i}") && arr_tmp[${i}]=x + done + printf '%s\n' "${arr_unique[@]}" +} + +# @description Check if a given array is empty. +# +# @example +# array=("a" "b" "c" "d") +# array::is_empty "${array[@]}" +# +# @arg $1 array Array to be checked. +# +# @exitcode 0 If the given array is empty. +# @exitcode 2 If the given array is not empty. +array::is_empty() { + declare -a array + local array=("$@") + if [ ${#array[@]} -eq 0 ]; then + return 0 + else + return 1 + fi +} +# @description Join array elements with a string. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s" "$(array::join "," "${array[@]}")" +# #Output +# a,b,c,d +# printf "%s" "$(array::join "" "${array[@]}")" +# #Output +# abcd +# +# @arg $1 string String to join the array elements (glue). +# @arg $2 array array to be joined with glue string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. +array::join() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare delimiter="${1}" + shift + printf "%s" "${1}" + shift + printf "%s" "${@/#/${delimiter}}" +} + +# @description Return an array with elements in reverse order. +# +# @example +# array=(1 2 3 4 5) +# printf "%s" "$(array::reverse "${array[@]}")" +# #Output +# 5 4 3 2 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The reversed array. +array::reverse() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare min=0 + declare -a array + array=("$@") + declare max=$((${#array[@]} - 1)) + + while [[ $min -lt $max ]]; do + # Swap current first and last elements + x="${array[$min]}" + array[$min]="${array[$max]}" + array[$max]="$x" + + # Move closer + ((min++, max--)) + done + printf '%s\n' "${array[@]}" +} + +# @description Returns a random item from the array. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s\n" "$(array::random_element "${array[@]}")" +# #Output +# c +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Random item out of the array. +array::random_element() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array + local array=("$@") + printf '%s\n' "${array[RANDOM % $#]}" +} + +# @description Sort an array from lowest to highest. +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# 1 +# 2 +# 4 5 +# a +# a c +# d +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout sorted array. +array::sort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort <<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Sort an array in reverse order (highest to lowest). +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# d +# a c +# a +# 4 5 +# 2 +# 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout reverse sorted array. +array::rsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort -r<<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Bubble sort an integer array from lowest to highest. +# This sort does not work on string array. +# @example +# iarr=(4 5 1 3) +# array::bsort "${iarr[@]}" +# #Output +# 1 +# 3 +# 4 +# 5 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout bubble sorted array. +array::bsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare tmp + declare arr=("$@") + for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do + for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do + if [[ ${arr[i]} -gt ${arr[j]} ]]; then + # echo $i $j ${arr[i]} ${arr[j]} + tmp=${arr[i]} + arr[i]=${arr[j]} + arr[j]=$tmp + fi + done + done + printf "%s\n" "${arr[@]}" +} + +# @description Merge two arrays. +# Pass the variable name of the array instead of value of the variable. +# @example +# a=("a" "c") +# b=("d" "c") +# array::merge "a[@]" "b[@]" +# #Output +# a +# c +# d +# c +# +# @arg $1 string variable name of first array. +# @arg $2 string variable name of second array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Merged array. +array::merge() { + [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr1=("${!1}") + declare -a arr2=("${!2}") + declare out=("${arr1[@]}" "${arr2[@]}") + printf "%s\n" "${out[@]}" +} diff --git a/src/bash-utility/src/check.sh b/src/bash-utility/src/check.sh new file mode 100644 index 000000000..2b7c1eb1d --- /dev/null +++ b/src/bash-utility/src/check.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# @file Check +# @brief Helper functions. + +# @description Check if the command exists in the system. +# +# @example +# check::command_exists "tput" +# +# @arg $1 string Command name to be searched. +# +# @exitcode 0 If the command exists. +# @exitcode 1 If the command does not exist. +# @exitcode 2 Function missing arguments. +check::command_exists() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + hash "${1}" 2> /dev/null +} + +# @description Check if the script is executed with sudo privilege. +# +# @example +# check::is_sudo +# +# @noargs +# +# @exitcode 0 If the script is executed with root privilege. +# @exitcode 1 If the script is not executed with root privilege +check::is_sudo() { + if [[ $(id -u) -ne 0 ]]; then + return 1 + fi +} diff --git a/src/bash-utility/src/collection.sh b/src/bash-utility/src/collection.sh new file mode 100644 index 000000000..9f0e244a2 --- /dev/null +++ b/src/bash-utility/src/collection.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash + +# @file Collection +# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. + +# @description Iterates over elements of collection and invokes iteratee for each element. +# Input to the function can be a pipe output, here-string or file. +# @example +# test_func(){ +# printf "print value: %s\n" "$1" +# return 0 +# } +# arr1=("a b" "c d" "a" "d") +# printf "%s\n" "${arr1[@]}" | collection::each "test_func" +# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach +# #output +# print value: a b +# print value: c d +# print value: a +# print value: d +# +# @example +# # If other function from this library is already used to process the array. +# # Then following method could be used to pass the array to the function. +# out=("$(array::dedupe "${arr1[@]}")") +# collection::each "test_func" <<< "${out[@]}" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output of iteratee function. +collection::each() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + done +} + +# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "4") +# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If iteratee function fails. +# @exitcode 2 Function missing arguments. +collection::every() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return 1 + fi + + done +} + +# @description Iterates over elements of array, returning all elements where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" +# #output +# 1 +# 2 +# 3 +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values matching the iteratee function. +collection::filter() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s\n" "${it}" + fi + done +} + +# @description Iterates over elements of collection, returning the first element where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("1" "2" "3" "a") +# check_a(){ +# [[ "$1" = "a" ]] +# } +# printf "%s\n" "${arr[@]}" | collection::find "check_a" +# #Output +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +# +# @stdout first array value matching the iteratee function. +collection::find() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s" "${it}" + return 0 + fi + done + + return 1 +} + +# @description Invokes the iteratee with each element passed as argument to the iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# opt=("-a" "-l") +# printf "%s\n" "${opt[@]}" | collection::invoke "ls" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output from the iteratee function. +collection::invoke() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a args=() + declare func="${1}" + while read -r it; do + args=("${args[@]}" "$it") + done + + eval "${func}" "${args[@]}" +} + +# @description Creates an array of values by running each element in array through iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3") +# add_one(){ +# i=${1} +# i=$(( i + 1 )) +# printf "%s\n" "$i" +# } +# printf "%s\n" "${arri[@]}" | collection::map "add_one" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output result of iteratee on value. +collection::map() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + declare out + + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + out="$("${func}")" + else + out="$("${func}" "$it")" + fi + + declare -i ret=$? + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + printf "%s\n" "${out}" + done +} + +# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" +# #Ouput +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values not matching the iteratee function. +# @see collection::filter +collection::reject() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + echo "$it" + fi + + done +} + +# @description Checks if iteratee returns true for any element of the array. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("a" "b" "3" "a") +# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If match successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +collection::some() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + + declare -i ret=$? + + if [[ $ret -eq 0 ]]; then + return 0 + fi + done + + return 1 +} diff --git a/src/bash-utility/src/date.sh b/src/bash-utility/src/date.sh new file mode 100644 index 000000000..41f071792 --- /dev/null +++ b/src/bash-utility/src/date.sh @@ -0,0 +1,744 @@ +#!/usr/bin/env bash + +# @file Date +# @brief Functions for manipulating dates. + +# @description Get current time in unix timestamp. +# +# @example +# echo "$(date::now)" +# #Output +# 1591554426 +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout current timestamp. +date::now() { + declare now + now="$(date --universal +%s)" || return $? + printf "%s" "${now}" +} + +# @description convert datetime string to unix timestamp. +# +# @example +# echo "$(date::epoc "2020-07-07 18:38")" +# #Output +# 1594143480 +# +# @arg $1 string date time in any format. +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp for specified datetime. +date::epoc() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare date + date=$(date -d "${1}" +"%s") || return $? + printf "%s" "${date}" +} + +# @description Add number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days_from "1594143480")" +# #Output +# 1594229880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months_from "1594143480")" +# #Output +# 1596821880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years_from "1594143480")" +# #Output +# 1625679480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::add_weeks_from "1594143480")" +# #Output +# 1594748280 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours_from "1594143480")" +# #Output +# 1594147080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes_from "1594143480")" +# #Output +# 1594143540 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds_from "1594143480")" +# #Output +# 1594143481 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days "1")" +# #Output +# 1591640826 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months "1")" +# #Output +# 1594146426 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years "1")" +# #Output +# 1623090426 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_weeks "1")" +# #Output +# 1592159226 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours "1")" +# #Output +# 1591558026 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes "1")" +# #Output +# 1591554486 +# +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds "1")" +# #Output +# 1591554427 +# +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days_from "1594143480")" +# #Output +# 1594057080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months_from "1594143480")" +# #Output +# 1591551480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years_from "1594143480")" +# #Output +# 1562521080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks_from "1594143480")" +# #Output +# 1593538680 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours_from "1594143480")" +# #Output +# 1594139880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes_from "1594143480")" +# #Output +# 1594143420 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds_from "1594143480")" +# #Output +# 1594143479 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days "1")" +# #Output +# 1588876026 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months "1")" +# #Output +# 1559932026 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years "1")" +# #Output +# 1591468026 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks "1")" +# #Output +# 1590949626 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours "1")" +# #Output +# 1591550826 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes "1")" +# #Output +# 1591554366 +# +# @arg $1 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds "1")" +# #Output +# 1591554425 +# +# @arg $1 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Format unix timestamp to human readable format. +# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. +# +# @example +# echo echo "$(date::format "1594143480")" +# #Output +# 2020-07-07 18:38:00 +# +# @arg $1 int unix timestamp. +# @arg $2 string format control characters based on `date` command (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate time string. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +date::format() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp format out + timestamp="${1}" + format="${2:-"%F %T"}" + out="$(date -d "@${timestamp}" +"${format}")" || return $? + printf "%s" "${out}" + +} diff --git a/src/bash-utility/src/debug.sh b/src/bash-utility/src/debug.sh new file mode 100644 index 000000000..82828734f --- /dev/null +++ b/src/bash-utility/src/debug.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# @file Debug +# @brief Functions to facilitate debugging scripts. + +# @description Prints the content of array as key value pair for easier debugging. +# Pass the variable name of the array instead of value of the variable. +# @example +# array=(foo bar baz) +# printf "Array\n" +# printarr "array" +# declare -A assoc_array +# assoc_array=([foo]=bar [baz]=foobar) +# printf "Assoc Array\n" +# printarr "assoc_array" +# #Output +# Array +# 0 = foo +# 1 = bar +# 2 = baz +# Assoc Array +# baz = foobar +# foo = bar +# +# @arg $1 string variable name of the array. +# +# @stdout Formatted key value of array. +debug::print_array() { + declare -n __arr="$1" + for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done +} + +# @description Function to print ansi escape sequence as is. +# This function helps debug ansi escape sequence in text by displaying the escape codes. +# +# @example +# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" +# debug::print_ansi "$txt" +# #Output +# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m +# +# @arg $1 string input with ansi escape sequence. +# +# @stdout Ansi escape sequence printed in output as is. +debug::print_ansi() { + #echo $(tr -dc '[:print:]'<<<$1) + printf "%s\n" "${1//$'\e'/\\e}" + +} diff --git a/src/bash-utility/src/file.sh b/src/bash-utility/src/file.sh new file mode 100644 index 000000000..71afd8476 --- /dev/null +++ b/src/bash-utility/src/file.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash + +# @file File +# @brief Functions for handling files. + +# @description Create temporary file. +# Function creates temporary file with random name. The temporary file will be deleted when script finishes. +# +# @example +# echo "$(file::make_temp_file)" +# #Output +# tmp.vgftzy +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp file. +# +# @stdout file name of temporary file created. +file::make_temp_file() { + declare temp_file + type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } + trap 'rm -f "${temp_file}"' EXIT + printf "%s" "${temp_file}" +} + +# @description Create temporary directory. +# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. +# +# @example +# echo "$(utility::make_temp_dir)" +# #Output +# tmp.rtfsxy +# +# @arg $1 string Temporary directory prefix +# @arg $2 string Flag to auto remove directory on exit trap (true) +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp directory. +# @exitcode 2 Missing arguments. +# +# @stdout directory name of temporary directory created. +file::make_temp_dir() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare temp_dir prefix="${1}" trap_rm="${2}" + temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") + if [[ -n "${trap_rm}" ]]; then + trap 'rm -rf "${temp_dir}"' EXIT + fi + printf "%s" "${temp_dir}" +} + +# @description Get only the filename from string path. +# +# @example +# echo "$(file::name "/path/to/test.md")" +# #Output +# test.md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout name of the file with extension. +file::name() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf "%s" "${1##*/}" +} + +# @description Get the basename of file from file name. +# +# @example +# echo "$(file::basename "/path/to/test.md")" +# #Output +# test +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout basename of the file. +file::basename() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare file basename + file="${1##*/}" + basename="${file%.*}" + + printf "%s" "${basename}" +} + +# @description Get the extension of file from file name. +# +# @example +# echo "$(file::extension "/path/to/test.md")" +# #Output +# md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 1 If no extension is found in the filename. +# @exitcode 2 Function missing arguments. +# +# @stdout extension of the file. +file::extension() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare file extension + file="${1##*/}" + extension="${file##*.}" + [[ "${file}" = "${extension}" ]] && return 1 + + printf "%s" "${extension}" +} + +# @description Get directory name from file path. +# +# @example +# echo "$(file::dirname "/path/to/test.md")" +# #Output +# /path/to +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout directory path. +file::dirname() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare tmp=${1:-.} + + [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } + tmp="${tmp%%"${tmp##*[!/]}"}" + + [[ ${tmp} != */* ]] && { printf '.\n' && return; } + tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" + + printf '%s' "${tmp:-/}" +} + +# @description Get absolute path of file or directory. +# +# @example +# file::full_path "../path/to/file.md" +# #Output +# /home/labbots/docs/path/to/file.md +# +# @arg $1 string relative or absolute path to file/direcotry. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# +# @stdout Absolute path to file/directory. +file::full_path() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare input="${1}" + if [[ -f ${input} ]]; then + printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" + elif [[ -d ${input} ]]; then + printf "%s\n" "$(cd "${input}" && pwd)" + else + return 1 + fi +} + +# @description Get mime type of provided input. +# +# @example +# file::mime_type "../src/file.sh" +# #Output +# application/x-shellscript +# +# @arg $1 string relative or absolute path to file/directory. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# @exitcode 3 If file or mimetype command not found in system. +# +# @stdout mime type of file/directory. +file::mime_type() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare mime_type + if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then + if type -p mimetype &> /dev/null; then + mime_type=$(mimetype --output-format %m "${1}") + elif type -p file &> /dev/null; then + mime_type=$(file --brief --mime-type "${1}") + else + return 3 + fi + else + return 1 + fi + printf "%s" "${mime_type}" +} + +# @description Search if a given pattern is found in file. +# +# @example +# file::contains_text "./file.sh" "^[ @[:alpha:]]*" +# file::contains_text "./file.sh" "@file" +# #Output +# 0 +# +# @arg $1 string relative or absolute path to file/directory. +# @arg $2 string search key or regular expression. +# +# @exitcode 0 If given search parameter is found in file. +# @exitcode 1 If search paramter not found in file. +# @exitcode 2 Function missing arguments. +file::contains_text() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + declare -r file="$1" + declare -r text="$2" + grep -q "$text" "$file" +} diff --git a/src/bash-utility/src/format.sh b/src/bash-utility/src/format.sh new file mode 100644 index 000000000..7dceb1d59 --- /dev/null +++ b/src/bash-utility/src/format.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash + +# @file Format +# @brief Functions to format provided input. + +# @internal +# @description Initialisation script when the code is sourced. +# +# @noargs +__init(){ +_check_terminal_window_size +} + +# @internal +# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. +# +# @noargs +_check_terminal_window_size() { + shopt -s checkwinsize && (: && :) + trap 'shopt -s checkwinsize; (:;:)' SIGWINCH +} +# @description Format seconds to human readable format. +# +# @example +# echo "$(format::human_readable_seconds "356786")" +# #Output +# 4 days 3 hours 6 minute(s) and 26 seconds +# +# @arg $1 int number of seconds. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +format::human_readable_seconds() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare T="${1}" + declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" + [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" + [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" + [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" + [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' + printf '%d seconds\n' "${SEC}" +} + +# @description Format bytes to human readable format. +# +# @example +# echo "$(format::bytes_to_human "2250")" +# #Output +# 2.19 KB +# +# @arg $1 int size in bytes. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted file size string. +format::bytes_to_human() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) + while ((b > 1024)); do + d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" + b=$((b / 1024)) && ((s++)) + done + printf "%s\n" "${b}${d} ${S[${s}]}" +} + +# @description Remove Ansi escape sequences from given text. +# +# @example +# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" +# #Output +# This is bold red text.This is green text. +# +# @arg $1 string Input text to be ansi stripped. +# +# @exitcode 0 If successful. +# +# @stdout Ansi stripped text. +format::strip_ansi() { + declare tmp esc tpa re + tmp="${1}" + esc=$(printf "\x1b") + tpa=$(printf "\x28") + re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" + while [[ "${tmp}" =~ $re ]]; do + tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" + done + printf "%s" "${tmp}" +} + +# @description Prints the given text to centre of terminal. +# +# @example +# format::text_center "This text is in centre of the terminal." "-" +# +# @arg $1 string Text to be printed. +# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::text_center() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare input="${1}" symbol="${2:- }" filler out no_ansi_out + no_ansi_out=$(format::strip_ansi "$input") + declare -i str_len=${#no_ansi_out} + declare -i filler_len="$(((COLUMNS - str_len) / 2))" + + [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" + for ((i = 0; i < filler_len; i++)); do + filler+="${symbol}" + done + + out="${filler}${input}${filler}" + [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" + printf "%s" "${out}" +} + +# @description Format String to print beautiful report. +# +# @example +# format::report "Initialising mission state" "Success" +# #Output +# Initialising mission state ....................................................................[ Success ] +# +# @arg $1 string Text to be printed on the left. +# @arg $2 string Text to be printed within the square brackets. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::report() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare symbol="." to_print y hl hlout out + declare input1="${1} " input2="${2}" + input2="[ $input2 ]" + to_print="$((COLUMNS * 60 / 100))" + y=$(( to_print - ( ${#input1} + ${#input2} ) )) + hl="$(printf '%*s' $y '')" + hlout=${hl// /${symbol}} + out="${input1}${hlout}${input2}" + printf "%s\n" "${out}" +} + +# @description Trim given text to width of the terminal window. +# +# @example +# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." +# #Output +# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. +# +# @arg $1 string Text of first sentence. +# @arg $2 string Text of second sentence (optional). +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout trimmed text. +format::trim_text_to_term() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare to_print out input1="$1" input2="$2" + if [[ $# = 1 ]]; then + to_print="$((COLUMNS * 93 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } + else + to_print="$((COLUMNS * 40 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } + to_print="$((COLUMNS * 53 / 100))" + { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } + fi + printf "%s" "$out" +} + +__init diff --git a/src/bash-utility/src/interaction.sh b/src/bash-utility/src/interaction.sh new file mode 100644 index 000000000..910b60e1c --- /dev/null +++ b/src/bash-utility/src/interaction.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# @file Interaction +# @brief Functions to enable interaction with the user. + +# @description Prompt yes or no question to the user. +# +# @example +# interaction::prompt_yes_no "Are you sure to proceed" "yes" +# #Output +# Are you sure to proceed (y/n)? [y] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer \[yes/no\] (optional). +# +# @exitcode 0 If user responds with yes. +# @exitcode 1 If user responds with no. +# @exitcode 2 Function missing arguments. +interaction::prompt_yes_no() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare def_arg response + def_arg="" + response="" + + case "${2}" in + [yY] | [yY][eE][sS]) + def_arg=y + ;; + [nN] | [nN][oO]) + def_arg=n + ;; + esac + + while :; do + printf "%s (y/n)? " "${1}" + [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -z "${response}" ]] && response="${def_arg}" + + case "${response}" in + [yY] | [yY][eE][sS]) + response=y + break + ;; + [nN] | [nN][oO]) + response=n + break + ;; + *) + response="" + ;; + esac + done + + [[ "${response}" = 'y' ]] && return 0 || return 1 +} + +# @description Prompt question to the user. +# +# @example +# interaction::prompt_response "Choose directory to install" "/home/path" +# #Output +# Choose directory to install? [/home/path] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer (optional). +# +# @exitcode 0 If user responds with answer. +# @exitcode 2 Function missing arguments. +# +# @stdout User entered answer to the question. +interaction::prompt_response() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare def_arg response + response="" + def_arg="${2}" + + while :; do + printf "%s ? " "${1}" + [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -n "${response}" ]] && break + + if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then + response="${def_arg}" + break + fi + done + + [[ "${response}" = "-" ]] && response="" + + printf "%s" "${response}" +} diff --git a/src/bash-utility/src/json.sh b/src/bash-utility/src/json.sh new file mode 100644 index 000000000..73476618e --- /dev/null +++ b/src/bash-utility/src/json.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# @file Json +# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. + +# @description Extract value from json based on key and position. +# Input to the function can be a pipe output, here-string or file. +# @example +# json::get_value "id" "1" < json_file +# json::get_value "id" <<< "${json_var}" +# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" +# +# @arg $1 string id of the field to fetch. +# @arg $2 int position of value to extract.Defaults to 1.(optional) +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string value of extracted key. +json::get_value() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare LC_ALL=C num="${2:-1}" + grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p +} diff --git a/src/bash-utility/src/misc.sh b/src/bash-utility/src/misc.sh new file mode 100644 index 000000000..fca9a75bf --- /dev/null +++ b/src/bash-utility/src/misc.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# @file Miscellaneous +# @brief Set of miscellaneous helper functions. + +# @internal +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +_is_terminal() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Check if internet connection is available. +# +# @example +# misc::check_internet_connection +# +# @noargs +# +# @exitcode 0 If script can connect to internet. +# @exitcode 1 If script cannot access internet. +misc::check_internet_connection() { + declare check_internet + if _is_terminal; then + check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" + else + check_internet="$(curl --compressed -Is google.com -m 10)" + fi + if [[ -z ${check_internet} ]]; then + return 1 + fi +} + +# @description Get list of process ids based on process name. +# +# @example +# misc::get_pid "chrome" +# #Ouput +# 25951 +# 26043 +# 26528 +# 26561 +# +# @arg $1 Name of the process to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout list of process ids. +misc::get_pid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + pgrep "${1}" +} + +# @description Get user id based on username. +# +# @example +# misc::get_uid "labbots" +# #Ouput +# 1000 +# +# @arg $1 username to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string uid for the username. +misc::get_uid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + user_id=$(id "${1}" 2> /dev/null) + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + printf "No user found with username: %s" "${1}\n" + return 1 + fi + + printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' + + unset user_id +} + +# @description Generate random uuid. +# +# @example +# misc::generate_uuid +# #Ouput +# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c +# +# @noargs +# +# @exitcode 0 If match successful. +# +# @stdout random generated uuid. +misc::generate_uuid() { + C="89ab" + + for ((N=0;N<16;++N)); do + B="$((RANDOM%256))" + + case "$N" in + 6) printf '4%x' "$((B%16))" ;; + 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; + + 3|5|7|9) + printf '%02x-' "$B" + ;; + + *) + printf '%02x' "$B" + ;; + esac + done + + printf '\n' +} diff --git a/src/bash-utility/src/os.sh b/src/bash-utility/src/os.sh new file mode 100644 index 000000000..5cfecfc78 --- /dev/null +++ b/src/bash-utility/src/os.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +# @file Operating System +# @brief Functions to detect Operating system and version. + +# @description Identify the OS the function is run on. +# +# @noargs +# +# @example +# os::detect_os +# #Output +# linux +# +# @exitcode 0 If OS is successfully detected. +# @exitcode 1 If unable to detect OS. +# +# @stdout Operating system name (linux, mac or windows). +os::detect_os() { + declare uname os + uname=$(command -v uname) + + case $("${uname}" | tr '[:upper:]' '[:lower:]') in + linux*) + os="linux" + ;; + darwin*) + os="mac" + ;; + msys* | cygwin* | mingw* | nt | win*) + # or possible 'bash on windows' + os="windows" + ;; + *) + return 1 + ;; + esac + printf "%s" "${os}" +} + +# @description Identify the distribution flavour of linux. +# +# @noargs +# +# @example +# os::detect_linux_distro +# #Output +# ubuntu +# @exitcode 0 If Linux distro is successfully detected. +# @exitcode 1 If unable to detect OS distro. +# +# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). +os::detect_linux_distro() { + declare distro + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro="${NAME}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro=$(lsb_release -si) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro="${DISTRIB_ID}" + elif [[ -f /etc/debian_version ]]; then + # Older Debian/Ubuntu/etc. + distro="debian" + elif [[ -f /etc/SuSe-release ]]; then + # Older SuSE/etc. + distro="suse" + elif [[ -f /etc/redhat-release ]]; then + # Older Red Hat, CentOS, etc. + distro="redhat" + else + return 1 + fi + printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' +} + +# @description Identify the Linux version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 20.04 +# +# @exitcode 0 If Linux version is successfully detected. +# @exitcode 1 If unable to detect Linux version. +# +# @stdout Linux OS version number (18.04, 20.04, etc.,). +os::detect_linux_version() { + declare distro_version + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro_version="${VERSION_ID}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro_version=$(lsb_release -sr) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro_version="${DISTRIB_RELEASE}" + else + return 1 + fi + printf "%s" "${distro_version}" +} + +# @description Identify the MacOS version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 10.15.7 +# @exitcode 0 If MacOS version is successfully detected. +# @exitcode 1 If unable to detect MacOS version. +# +# @stdout MacOS version number (10.15.6, etc.,) +os::detect_mac_version() { + if [[ "$(os::detect_os)" = "mac" ]]; then + declare mac_version + mac_version="$(sw_vers -productVersion)" + printf "%s" "${mac_version}" + else + return 1 + fi +} diff --git a/src/bash-utility/src/string.sh b/src/bash-utility/src/string.sh new file mode 100644 index 000000000..aa522cb55 --- /dev/null +++ b/src/bash-utility/src/string.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash + +# @file String +# @brief Functions for string operations and manipulations. + +# @description Strip whitespace from the beginning and end of a string. +# +# @example +# echo "$(string::trim " Hello World! ")" +# #Output +# Hello World! +# +# @arg $1 string The string to be trimmed. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The trimmed string. +string::trim() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + : "${1#"${1%%[![:space:]]*}"}" + : "${_%"${_##*[![:space:]]}"}" + printf '%s\n' "$_" +} + +# @description Split a string to array by a delimiter. +# +# @example +# array=( $(string::split "a,b,c" ",") ) +# printf "%s" "$(string::split "Hello!World" "!")" +# #Output +# Hello +# World +# +# @arg $1 string The input string. +# @arg $2 string The delimiter string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. +string::split() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr=() + IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" + printf '%s\n' "${arr[@]}" +} + +# @description Strip characters from the beginning of a string. +# +# @example +# echo "$(string::lstrip "Hello World!" "He")" +# #Output +# llo World! +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::lstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1##$2}" +} + +# @description Strip characters from the end of a string. +# +# @example +# echo "$(string::rstrip "Hello World!" "d!")" +# #Output +# Hello Worl +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::rstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1%%$2}" +} + +# @description Make a string lowercase. +# +# @example +# echo "$(string::to_lower "HellO")" +# #Output +# hello +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the lowercased string. +string::to_lower() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1,,}" + else + printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' + fi +} + +# @description Make a string all uppercase. +# +# @example +# echo "$(string::to_upper "HellO")" +# #Output +# HELLO +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the uppercased string. +string::to_upper() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1^^}" + else + printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' + fi +} + +# @description Check whether the search string exists within the input string. +# +# @example +# string::contains "Hello World!" "lo" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_contains hello he + [[ "${1}" == *${2}* ]] +} + +# @description Check whether the input string starts with key string. +# +# @example +# string::starts_with "Hello World!" "He" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::starts_with() { + # Usage: string_starts_with hello he + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + [[ "${1}" == ${2}* ]] +} + +# @description Check whether the input string ends with key string. +# +# @example +# string::ends_with "Hello World!" "d!" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::ends_with() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_ends_wit hello lo + [[ "${1}" == *${2} ]] +} + +# @description Check whether the input string matches the given regex. +# +# @example +# string::regex "HELLO" "^[A-Z]*$" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::regex() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${1} =~ ${2} ]]; then + return 0 + else + return 1 + fi + +} diff --git a/src/bash-utility/src/terminal.sh b/src/bash-utility/src/terminal.sh new file mode 100644 index 000000000..d73331d7a --- /dev/null +++ b/src/bash-utility/src/terminal.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# @file Terminal +# @brief Set of useful terminal functions. + +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +terminal::is_term() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Detect profile rc file for zsh and bash of current script running user. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +# +# @stdout path to the profile file. +terminal::detect_profile() { + declare CURRENT_SHELL="${SHELL##*/}" + case "${CURRENT_SHELL}" in + 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; + 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; + *) if [[ -f "${HOME}/.profile" ]]; then + DETECTED_PROFILE="${HOME}/.profile" + else + printf "No compaitable shell file\n" && exit 1 + fi ;; + esac + printf "%s\n" "${DETECTED_PROFILE}" +} + +# @description Clear the output in terminal on the specified line number. +# This function clears line only on terminal. +# +# @arg $1 Line number to clear. Defaults to 1. (optional) +# +# @exitcode 0 If script is run on terminal. +# +# @stdout clear line ansi code. +terminal::clear_line() { + if terminal::is_term; then + declare line=${1:-1} + printf "\033[%sA\033[2K" "${line}" + fi +} diff --git a/src/bash-utility/src/validation.sh b/src/bash-utility/src/validation.sh new file mode 100644 index 000000000..37fde1cbc --- /dev/null +++ b/src/bash-utility/src/validation.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash + +# @file Validation +# @brief Functions to perform validation on given data. + +# @description Validate whether a given input is a valid email address or not. +# +# @example +# test='test@gmail.com' +# validation::email "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string input email address to validate. +# +# @exitcode 0 If provided input is an email address. +# @exitcode 1 If provided input is not an email address. +# @exitcode 2 Function missing arguments. +validation::email() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare email_re + email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" + [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 +} + +# @description Validate whether a given input is a valid IP V4 address. +# +# @example +# ips=' +# 4.2.2.2 +# a.b.c.d +# 192.168.1.1 +# 0.0.0.0 +# 255.255.255.255 +# 255.255.255.256 +# 192.168.0.1 +# 192.168.0 +# 1234.123.123.123 +# 0.192.168.1 +# ' +# for ip in $ips; do +# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi +# printf "%-20s: %s\n" "$ip" "$stat" +# done +# #Output +# 4.2.2.2 : good +# a.b.c.d : bad +# 192.168.1.1 : good +# 0.0.0.0 : good +# 255.255.255.255 : good +# 255.255.255.256 : bad +# 192.168.0.1 : good +# 192.168.0 : bad +# 1234.123.123.123 : bad +# 0.192.168.1 : good +# +# @arg $1 string input IPv4 address. +# +# @exitcode 0 If provided input is a valid IPv4. +# @exitcode 1 If provided input is not a valid IPv4. +# @exitcode 2 Function missing arguments. +validation::ipv4() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare ip="${1}" + declare IFS=. + # shellcheck disable=SC2206 + declare -a a=($ip) + [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 + # Test values of quads + declare quad + for quad in {0..3}; do + [[ "${a[$quad]}" -gt 255 ]] && return 1 + done + return 0 +} + +# @description Validate whether a given input is a valid IP V6 address. +# +# @example +# ips=' +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 +# fe80::1ff:fe23:4567:890a +# fe80::1ff:fe23:4567:890a%eth2 +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar +# fezy::1ff:fe23:4567:890a +# :: +# 2001:db8:: +# ' +# for ip in $ips; do +# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi +# printf "%-50s= %s\n" "$ip" "$stat" +# done +# #Output +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good +# fe80::1ff:fe23:4567:890a = good +# fe80::1ff:fe23:4567:890a%eth2 = good +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad +# fezy::1ff:fe23:4567:890a = bad +# :: = good +# 2001:db8:: = good +# +# @arg $1 string input IPv6 address. +# +# @exitcode 0 If provided input is a valid IPv6. +# @exitcode 1 If provided input is not a valid IPv6. +# @exitcode 2 Function missing arguments. +validation::ipv6() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare ip="${1}" + declare 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]))$" + + [[ "${ip}" =~ $re ]] && return 0 || return 1 +} + +# @description Validate if given variable is entirely alphabetic characters. +# +# @example +# test='abcABC' +# validation::alpha "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is only alpha characters. +# @exitcode 1 If input contains any non alpha characters. +# @exitcode 2 Function missing arguments. +validation::alpha() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable contains only alpha-numeric characters. +# +# @example +# test='abc123' +# validation::alpha_num "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is an alpha-numeric. +# @exitcode 1 If input is not an alpha-numeric. +# @exitcode 2 Function missing arguments. +validation::alpha_num() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alnum:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. +# +# @example +# test='abc-ABC_cD' +# validation::alpha_dash "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is valid. +# @exitcode 1 If input the input is not valid. +# @exitcode 2 Function missing arguments. +validation::alpha_dash() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]_-]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. +# +# @arg $1 string Version number to check (eg: 1.0.1) +# $arg $2 string Version number to check (eg: 1.0.1) +# +# @example +# test='abc-ABC_cD' +# validation::version_comparison "12.0.1" "12.0.1" +# echo $? +# #Output +# 0 +# +# @exitcode 0 version number is equal. +# @exitcode 1 $1 version number is greater than $2. +# @exitcode 2 $1 version number is less than $2. +# @exitcode 3 Function is missing required arguments. +# @exitcode 4 Provided input argument is in invalid format. +validation::version_comparison() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 + + declare regex="^[.0-9]*$" + ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 + ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 + + if [[ "$1" == "$2" ]]; then + return 0 + fi + declare IFS=. + declare -a ver1 ver2 + read -r -a ver1 <<<"${1}" + read -r -a ver2 <<<"${2}" + # fill empty fields in ver1 with zeros + for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do + ver1[i]=0 + done + for ((i = 0; i < ${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + # fill empty fields in ver2 with zeros + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 + fi + done + return 0 +} diff --git a/src/bash-utility/src/variable.sh b/src/bash-utility/src/variable.sh new file mode 100644 index 000000000..67e9fab55 --- /dev/null +++ b/src/bash-utility/src/variable.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# @file Variable +# @brief Functions for handling variables. + +# @description Check if given variable is array. +# Pass the variable name instead of value of the variable. +# +# @example +# arr=("a" "b" "c") +# variable::is_array "arr" +# #Output +# 0 +# +# @arg $1 string name of the variable to check. +# +# @exitcode 0 If input is array. +# @exitcode 1 If input is not an array. +variable::is_array() { + if [[ -z "${1}" ]]; then + return 1 + else + declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 + fi + return 1 +} + +# @description Check if given variable is a number. +# +# @example +# variable::is_numeric "1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is number. +# @exitcode 1 If input is not a number. +variable::is_numeric() { + declare re='^[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is an integer. +# +# @example +# variable::is_int "+1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is an integer. +# @exitcode 1 If input is not an integer. +variable::is_int() { + declare re='^[+-]?[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a float. +# +# @example +# variable::is_float "+1234.0" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a float. +# @exitcode 1 If input is not a float. +variable::is_float() { + declare re='^[+-]?[0-9]+.?[0-9]*$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a boolean. +# +# @example +# variable::is_bool "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a boolean. +# @exitcode 1 If input is not a boolean. +variable::is_bool() { + [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 +} + +# @description Check if given variable is a true. +# +# @example +# variable::is_true "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is true. +# @exitcode 1 If input is not true. +variable::is_true() { + [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 +} + +# @description Check if given variable is false. +# +# @example +# variable::is_false "false" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is false. +# @exitcode 1 If input is not false. +variable::is_false() { + [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 +} + +# @description Check if given variable is empty or null. +# +# @example +# test='' +# variable::is_empty_or_null $test +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is empty or null. +# @exitcode 1 If input is not empty or null. +variable::is_empty_or_null() { + [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 +} diff --git a/src/configng/README.md b/src/configng/README.md new file mode 100644 index 000000000..1653df4c9 --- /dev/null +++ b/src/configng/README.md @@ -0,0 +1,62 @@ +# configng +This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility) +embedded in this project. This allows for functional programming in Bash and also modernizes +the monolithic nature of armbian-config. Error handling and validation are also included. +The idea is to provide an API in Bash that can be called from a TUI, GUI or CLI. Please +follow the coding standards which follow Bash Utility functions. + +Why Bash? Well, because it's going to be in every distribution. Striped down distributions +may not include Python, C/C++, etc. build/runtime environments + +## Quick start +* `sudo apt install git` +* `cd ~/` +* `git clone https://github.com/armbian/configng.git` +* `cd ~/configng/test` +* `sudo ./cpu_test.sh` +If all goes well you should see all the functions in cpu.sh called and output diaplayed. + +## Coding standards +[Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, +but fundementally look at the code in Bash Utility: +``` +# @description Strip characters from the beginning of a string. +# +# @example +# echo "$(string::lstrip "Hello World!" "He")" +# #Output +# llo World! +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::lstrip() { +[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 +printf '%s\n' "${1##$2}" +} +``` + +Functions should follow filename::func_name style. Then you can tell just from the name which +file the function is located in. Return codes should also follow a similar pattern: +* 0 Successful +* 1 Not found +* 2 Function missing arguments +* 3-255 all other errors + +Validate values: +``` +# Validate minimum frequency is <= maximum frequency +[ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 +``` + +Return values should use stdout: +``` +# Return value +printf '%s\n' "$(cat $file)" +``` + +Only use sudo when needed and never run as root! diff --git a/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md b/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..461c926b9 --- /dev/null +++ b/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at + +For answers to common questions about this code of conduct, see + + +[homepage]: https://www.contributor-covenant.org diff --git a/src/configng/functions/bash-utility-master/CONTRIBUTING.md b/src/configng/functions/bash-utility-master/CONTRIBUTING.md new file mode 100644 index 000000000..aa401df88 --- /dev/null +++ b/src/configng/functions/bash-utility-master/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing to Bash-Utility + +:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: +The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Table of Contents +- [Code Contributions](#code-contributions) +- [Code Guidelines](#code-guidelines) + - [Styleguide](#styleguide) + - [Bashdoc guideline](#bashdoc-guideline) +- [Documentation](#documentation) +- [Commit Guidelines](#commit-guidelines) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Contact](#contact) + +## Code Contributions + +Great, the more, the merrier. + +Sane code contributions are always welcome, whether to the code or documentation. + +Before making a pull request, make sure to follow below guidelines: + +### Code Guidelines + +#### Styleguide + +- Variable names must be meaningful and self-documenting. +- Long variable names must be structured by underscores to improve legibility. +- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE) +- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data) +- Variable names can be alphanumeric with underscores. No special characters in variable names. +- Variables name must not start with number. +- Variables within function must be declared. So the scope of variable is restricted to the function. +- Avoid accessing global variables within functions. +- Function names must be all lower case with underscores to seperate words (snake_case). +- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`) +- Try using bash builtins and string substitution as much as possible. +- Use printf everywhere instead of echo. +- Before adding a new logic, be sure to check the existing code. +- Make sure to add the function in appropriate section based on its operation. +- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command: + + ```shell + shfmt upload.sh + ``` + + The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags. + You can also install shfmt for various editors, refer their repo for information. + Note: This is strictly necessary to maintain consistency, do not skip. + +- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason. + +#### Bashdoc guideline + +The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is +properly generated by the generator. + +Follow the below bashdoc template to add function introductory comment. + +```bash +# @description Multiline description goes here and +# there +# +# @example +# sample::function a b c +# echo 123 +# +# @arg $1 string Some arg. +# @arg $2 any Rest of arguments. +# +# @noargs +# +# @exitcode 0 If successfull. +# @exitcode >0 On failure +# @exitcode 5 On some error. +# +# @stdout Path to something. +# +# @see sample::other_function(() +sample::function() { +} +``` + +- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used. +- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment. +- Make sure to document the exitcode emitted by the function. +- If the function is similar to other function add a reference to function using @see tag. + +### Documentation + +- Refrain from making unnecessary newlines or whitespace. +- Use pure markdown as much as possible, html is accepted but shouldn't be a priority. +- The markdown must pass RemarkLint checks. +- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc. + + ```bash + ./bin/generate_readme.sh -f README.md -s src/ + ``` + +### Commit Guidelines + +It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted. + +It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message. + +The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation. + +Before committing check for unnecessary whitespace with `git diff --check`. + +### Pull Request Guidelines + +The following guidelines will increase the likelihood that your pull request will get accepted: + +- Follow the commit and code guidelines. +- Keep the patches on topic and focused. +- Try to avoid unnecessary formatting and clean-up where reasonable. + +A pull request should contain the following: + +- At least one commit (all of which should follow the Commit Guidelines). +- Title that summarises the issue/feature. +- Description that briefly summarises the changes. + +After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread. + +## Contact + +For further inquiries, you can contact the developer by opening an issue on the repository. diff --git a/src/configng/functions/bash-utility-master/LICENSE b/src/configng/functions/bash-utility-master/LICENSE new file mode 100644 index 000000000..99dd0836b --- /dev/null +++ b/src/configng/functions/bash-utility-master/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 labbots + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/configng/functions/bash-utility-master/README.md b/src/configng/functions/bash-utility-master/README.md new file mode 100644 index 000000000..9bc1f9484 --- /dev/null +++ b/src/configng/functions/bash-utility-master/README.md @@ -0,0 +1,3026 @@ +

Bash Utility

+ +

+Stars +License + +

+

+Gh-pages Status +Website +

+

+Total number of Library functions +

+

+ +

+Bash library which provides utility functions and helpers for functional programming in Bash. + +Detailed documentation is available at + + + +## Table of Contents + +- [Installation](#installation) + - [Method 1 - Git Submodules](#method-1---git-submodules) + - [Method 2 - Git Clone](#method-2---git-clone) + - [Method 3 - Direct Download](#method-3---direct-download) +- [Usage](#usage) +- [Array](#array) + - [array::contains()](#arraycontains) + - [array::dedupe()](#arraydedupe) + - [array::is_empty()](#arrayis_empty) + - [array::join()](#arrayjoin) + - [array::reverse()](#arrayreverse) + - [array::random_element()](#arrayrandom_element) + - [array::sort()](#arraysort) + - [array::rsort()](#arrayrsort) + - [array::bsort()](#arraybsort) + - [array::merge()](#arraymerge) +- [Check](#check) + - [check::command_exists()](#checkcommand_exists) + - [check::is_sudo()](#checkis_sudo) +- [Collection](#collection) + - [collection::each()](#collectioneach) + - [collection::every()](#collectionevery) + - [collection::filter()](#collectionfilter) + - [collection::find()](#collectionfind) + - [collection::invoke()](#collectioninvoke) + - [collection::map()](#collectionmap) + - [collection::reject()](#collectionreject) + - [collection::some()](#collectionsome) +- [Date](#date) + - [date::now()](#datenow) + - [date::epoc()](#dateepoc) + - [date::add_days_from()](#dateadd_days_from) + - [date::add_months_from()](#dateadd_months_from) + - [date::add_years_from()](#dateadd_years_from) + - [date::add_weeks_from()](#dateadd_weeks_from) + - [date::add_hours_from()](#dateadd_hours_from) + - [date::add_minutes_from()](#dateadd_minutes_from) + - [date::add_seconds_from()](#dateadd_seconds_from) + - [date::add_days()](#dateadd_days) + - [date::add_months()](#dateadd_months) + - [date::add_years()](#dateadd_years) + - [date::add_weeks()](#dateadd_weeks) + - [date::add_hours()](#dateadd_hours) + - [date::add_minutes()](#dateadd_minutes) + - [date::add_seconds()](#dateadd_seconds) + - [date::sub_days_from()](#datesub_days_from) + - [date::sub_months_from()](#datesub_months_from) + - [date::sub_years_from()](#datesub_years_from) + - [date::sub_weeks_from()](#datesub_weeks_from) + - [date::sub_hours_from()](#datesub_hours_from) + - [date::sub_minutes_from()](#datesub_minutes_from) + - [date::sub_seconds_from()](#datesub_seconds_from) + - [date::sub_days()](#datesub_days) + - [date::sub_months()](#datesub_months) + - [date::sub_years()](#datesub_years) + - [date::sub_weeks()](#datesub_weeks) + - [date::sub_hours()](#datesub_hours) + - [date::sub_minutes()](#datesub_minutes) + - [date::sub_seconds()](#datesub_seconds) + - [date::format()](#dateformat) +- [Debug](#debug) + - [debug::print_array()](#debugprint_array) + - [debug::print_ansi()](#debugprint_ansi) +- [File](#file) + - [file::make_temp_file()](#filemake_temp_file) + - [file::make_temp_dir()](#filemake_temp_dir) + - [file::name()](#filename) + - [file::basename()](#filebasename) + - [file::extension()](#fileextension) + - [file::dirname()](#filedirname) + - [file::full_path()](#filefull_path) + - [file::mime_type()](#filemime_type) + - [file::contains_text()](#filecontains_text) +- [Format](#format) + - [format::human_readable_seconds()](#formathuman_readable_seconds) + - [format::bytes_to_human()](#formatbytes_to_human) + - [format::strip_ansi()](#formatstrip_ansi) + - [format::text_center()](#formattext_center) + - [format::report()](#formatreport) + - [format::trim_text_to_term()](#formattrim_text_to_term) +- [Interaction](#interaction) + - [interaction::prompt_yes_no()](#interactionprompt_yes_no) + - [interaction::prompt_response()](#interactionprompt_response) +- [Json](#json) + - [json::get_value()](#jsonget_value) +- [Miscellaneous](#miscellaneous) + - [misc::check_internet_connection()](#misccheck_internet_connection) + - [misc::get_pid()](#miscget_pid) + - [misc::get_uid()](#miscget_uid) + - [misc::generate_uuid()](#miscgenerate_uuid) +- [Operating System](#operating-system) + - [os::detect_os()](#osdetect_os) + - [os::detect_linux_distro()](#osdetect_linux_distro) + - [os::detect_linux_version()](#osdetect_linux_version) + - [os::detect_mac_version()](#osdetect_mac_version) +- [String](#string) + - [string::trim()](#stringtrim) + - [string::split()](#stringsplit) + - [string::lstrip()](#stringlstrip) + - [string::rstrip()](#stringrstrip) + - [string::to_lower()](#stringto_lower) + - [string::to_upper()](#stringto_upper) + - [string::contains()](#stringcontains) + - [string::starts_with()](#stringstarts_with) + - [string::ends_with()](#stringends_with) + - [string::regex()](#stringregex) +- [Terminal](#terminal) + - [terminal::is_term()](#terminalis_term) + - [terminal::detect_profile()](#terminaldetect_profile) + - [terminal::clear_line()](#terminalclear_line) +- [Validation](#validation) + - [validation::email()](#validationemail) + - [validation::ipv4()](#validationipv4) + - [validation::ipv6()](#validationipv6) + - [validation::alpha()](#validationalpha) + - [validation::alpha_num()](#validationalpha_num) + - [validation::alpha_dash()](#validationalpha_dash) + - [validation::version_comparison()](#validationversion_comparison) +- [Variable](#variable) + - [variable::is_array()](#variableis_array) + - [variable::is_numeric()](#variableis_numeric) + - [variable::is_int()](#variableis_int) + - [variable::is_float()](#variableis_float) + - [variable::is_bool()](#variableis_bool) + - [variable::is_true()](#variableis_true) + - [variable::is_false()](#variableis_false) + - [variable::is_empty_or_null()](#variableis_empty_or_null) +- [Inspired By](#inspired-by) +- [License](#license) + + +## Installation +The script can be installed and sourced using following methods. + +### Method 1 - Git Submodules +If the library is used inside a git project then git submodules can be used to install the library to the project. +Following command will initialize git submodule and download the library to `./vendor/bash-utility` folder. + +```shell +git submodule init +git submodule add -b master https://github.com/labbots/bash-utility vendor/bash-utility +``` + +To Update submodules to latest code execute the following command. + +```shell +git submodule update --rebase --remote +``` +Once the submodule is added or updated, make sure to commit changes to your repository. + +```shell +git add . +git commit -m 'Added/updated bash-utility library.' +``` +**Note:** When cloning your repository, use `--recurse-submodules` flag to `git clone` command to install the git sub modules. + +### Method 2 - Git Clone +If you don't want to use git submodules, you can use `git clone` to download library and then move the files to desired location manually. + +The below command will clone the repository to `vendor/bash-utility` folder in current working directory. + +```shell +git clone https://github.com/labbots/bash-utility.git ./vendor/bash-utility +``` +### Method 3 - Direct Download +If you do not have git installed, you can download the archive of the latest version of the library. Extract the zip file to appropriate folder by following the below command. + +```shell +wget https://github.com/labbots/bash-utility/archive/master.zip +unzip -q master.zip -d tmp +mkdir -p vendor/bash-utility +mv tmp/bash-utility-master vendor/bash-utility +rm tmp +``` + +## Usage +Bash utility functions can be used by simply sourcing the library script file to your own script. +To access all the functions within the bash-utility library, you could import the main bash file as follows. + +```shell +source "vendor/bash-utility/bash-utility.sh" +``` + +You can also only use the necessary library functions by only importing the required function files. + +```shell +source "vendor/bash-utility/src/array.sh" +``` + + + +## Array + +Functions for array operations and manipulations. + +### array::contains() + +Check if item exists in the given array. + +#### Arguments + +- **$1** (mixed): Item to search (needle). +- **$2** (array): array to be searched (haystack). + +#### Exit codes + +- **0**: If successful. +- **1**: If no match found in the array. +- **2**: Function missing arguments. + +#### Example + +```bash +array=("a" "b" "c") +array::contains "c" ${array[@]} +#Output +0 +``` + +### array::dedupe() + +Remove duplicate items from the array. + +#### Arguments + +- **$1** (array): Array to be deduped. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Deduplicated array. + +#### Example + +```bash +array=("a" "b" "a" "c") +printf "%s" "$(array::dedupe ${array[@]})" +#Output +a +b +c +``` + +### array::is_empty() + +Check if a given array is empty. + +#### Arguments + +- **$1** (array): Array to be checked. + +#### Exit codes + +- **0**: If the given array is empty. +- **2**: If the given array is not empty. + +#### Example + +```bash +array=("a" "b" "c" "d") +array::is_empty "${array[@]}" +``` + +### array::join() + +Join array elements with a string. + +#### Arguments + +- **$1** (string): String to join the array elements (glue). +- **$2** (array): array to be joined with glue string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- String containing a string representation of all the array elements in the same order,with the glue string between each element. + +#### Example + +```bash +array=("a" "b" "c" "d") +printf "%s" "$(array::join "," "${array[@]}")" +#Output +a,b,c,d +printf "%s" "$(array::join "" "${array[@]}")" +#Output +abcd +``` + +### array::reverse() + +Return an array with elements in reverse order. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- The reversed array. + +#### Example + +```bash +array=(1 2 3 4 5) +printf "%s" "$(array::reverse "${array[@]}")" +#Output +5 4 3 2 1 +``` + +### array::random_element() + +Returns a random item from the array. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Random item out of the array. + +#### Example + +```bash +array=("a" "b" "c" "d") +printf "%s\n" "$(array::random_element "${array[@]}")" +#Output +c +``` + +### array::sort() + +Sort an array from lowest to highest. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- sorted array. + +#### Example + +```bash +sarr=("a c" "a" "d" 2 1 "4 5") +array::array_sort "${sarr[@]}" +#Output +1 +2 +4 5 +a +a c +d +``` + +### array::rsort() + +Sort an array in reverse order (highest to lowest). + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- reverse sorted array. + +#### Example + +```bash +sarr=("a c" "a" "d" 2 1 "4 5") +array::array_sort "${sarr[@]}" +#Output +d +a c +a +4 5 +2 +1 +``` + +### array::bsort() + +Bubble sort an integer array from lowest to highest. +This sort does not work on string array. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- bubble sorted array. + +#### Example + +```bash +iarr=(4 5 1 3) +array::bsort "${iarr[@]}" +#Output +1 +3 +4 +5 +``` + +### array::merge() + +Merge two arrays. +Pass the variable name of the array instead of value of the variable. + +#### Arguments + +- **$1** (string): variable name of first array. +- **$2** (string): variable name of second array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Merged array. + +#### Example + +```bash +a=("a" "c") +b=("d" "c") +array::merge "a[@]" "b[@]" +#Output +a +c +d +c +``` + +## Check + +Helper functions. + +### check::command_exists() + +Check if the command exists in the system. + +#### Arguments + +- **$1** (string): Command name to be searched. + +#### Exit codes + +- **0**: If the command exists. +- **1**: If the command does not exist. +- **2**: Function missing arguments. + +#### Example + +```bash +check::command_exists "tput" +``` + +### check::is_sudo() + +Check if the script is executed with sudo privilege. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If the script is executed with root privilege. +- **1**: If the script is not executed with root privilege + +#### Example + +```bash +check::is_sudo +``` + +## Collection + +(Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. + +### collection::each() + +Iterates over elements of collection and invokes iteratee for each element. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output of iteratee function. + +#### Example + +```bash +test_func(){ + printf "print value: %s\n" "$1" + return 0 + } +arr1=("a b" "c d" "a" "d") +printf "%s\n" "${arr1[@]}" | collection::each "test_func" +collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach +#output + print value: a b + print value: c d + print value: a + print value: d +``` + +#### Example + +```bash +# If other function from this library is already used to process the array. +# Then following method could be used to pass the array to the function. +out=("$(array::dedupe "${arr1[@]}")") +collection::each "test_func" <<< "${out[@]}" +``` + +### collection::every() + +Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **1**: If iteratee function fails. +- **2**: Function missing arguments. + +#### Example + +```bash +arri=("1" "2" "3" "4") +printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" +``` + +### collection::filter() + +Iterates over elements of array, returning all elements where iteratee returns true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- array values matching the iteratee function. + +#### Example + +```bash +arri=("1" "2" "3" "a") +printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" +#output +1 +2 +3 +``` + +### collection::find() + +Iterates over elements of collection, returning the first element where iteratee returns true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Output on stdout + +- first array value matching the iteratee function. + +#### Example + +```bash +arr=("1" "2" "3" "a") +check_a(){ + [[ "$1" = "a" ]] +} +printf "%s\n" "${arr[@]}" | collection::find "check_a" +#Output +a +``` + +### collection::invoke() + +Invokes the iteratee with each element passed as argument to the iteratee. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output from the iteratee function. + +#### Example + +```bash +opt=("-a" "-l") +printf "%s\n" "${opt[@]}" | collection::invoke "ls" +``` + +### collection::map() + +Creates an array of values by running each element in array through iteratee. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output result of iteratee on value. + +#### Example + +```bash +arri=("1" "2" "3") +add_one(){ + i=${1} + i=$(( i + 1 )) + printf "%s\n" "$i" +} +printf "%s\n" "${arri[@]}" | collection::map "add_one" +``` + +### collection::reject() + +The opposite of filter function; this method returns the elements of collection that iteratee does not return true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- array values not matching the iteratee function. + +#### Example + +```bash +arri=("1" "2" "3" "a") +printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" +#Ouput +a +``` + +#### See also + +- [collection::filter](#collectionfilter) + +### collection::some() + +Checks if iteratee returns true for any element of the array. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If match successful. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +arr=("a" "b" "3" "a") +printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" +``` + +## Date + +Functions for manipulating dates. + +### date::now() + +Get current time in unix timestamp. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- current timestamp. + +#### Example + +```bash +echo "$(date::now)" +#Output +1591554426 +``` + +### date::epoc() + +convert datetime string to unix timestamp. + +#### Arguments + +- **$1** (string): date time in any format. + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp for specified datetime. + +#### Example + +```bash +echo "$(date::epoc "2020-07-07 18:38")" +#Output +1594143480 +``` + +### date::add_days_from() + +Add number of days from specified timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_days_from "1594143480")" +#Output +1594229880 +``` + +### date::add_months_from() + +Add number of months from specified timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_months_from "1594143480")" +#Output +1596821880 +``` + +### date::add_years_from() + +Add number of years from specified timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_years_from "1594143480")" +#Output +1625679480 +``` + +### date::add_weeks_from() + +Add number of weeks from specified timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_weeks_from "1594143480")" +#Output +1594748280 +``` + +### date::add_hours_from() + +Add number of hours from specified timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_hours_from "1594143480")" +#Output +1594147080 +``` + +### date::add_minutes_from() + +Add number of minutes from specified timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_minutes_from "1594143480")" +#Output +1594143540 +``` + +### date::add_seconds_from() + +Add number of seconds from specified timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_seconds_from "1594143480")" +#Output +1594143481 +``` + +### date::add_days() + +Add number of days from current day timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_days "1")" +#Output +1591640826 +``` + +### date::add_months() + +Add number of months from current day timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_months "1")" +#Output +1594146426 +``` + +### date::add_years() + +Add number of years from current day timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_years "1")" +#Output +1623090426 +``` + +### date::add_weeks() + +Add number of weeks from current day timestamp. +If number of weeks not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_weeks "1")" +#Output +1592159226 +``` + +### date::add_hours() + +Add number of hours from current day timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_hours "1")" +#Output +1591558026 +``` + +### date::add_minutes() + +Add number of minutes from current day timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_minutes "1")" +#Output +1591554486 +``` + +### date::add_seconds() + +Add number of seconds from current day timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_seconds "1")" +#Output +1591554427 +``` + +### date::sub_days_from() + +Subtract number of days from specified timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_days_from "1594143480")" +#Output +1594057080 +``` + +### date::sub_months_from() + +Subtract number of months from specified timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_months_from "1594143480")" +#Output +1591551480 +``` + +### date::sub_years_from() + +Subtract number of years from specified timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_years_from "1594143480")" +#Output +1562521080 +``` + +### date::sub_weeks_from() + +Subtract number of weeks from specified timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_weeks_from "1594143480")" +#Output +1593538680 +``` + +### date::sub_hours_from() + +Subtract number of hours from specified timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_hours_from "1594143480")" +#Output +1594139880 +``` + +### date::sub_minutes_from() + +Subtract number of minutes from specified timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_minutes_from "1594143480")" +#Output +1594143420 +``` + +### date::sub_seconds_from() + +Subtract number of seconds from specified timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_seconds_from "1594143480")" +#Output +1594143479 +``` + +### date::sub_days() + +Subtract number of days from current day timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_days "1")" +#Output +1588876026 +``` + +### date::sub_months() + +Subtract number of months from current day timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_months "1")" +#Output +1559932026 +``` + +### date::sub_years() + +Subtract number of years from current day timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_years "1")" +#Output +1591468026 +``` + +### date::sub_weeks() + +Subtract number of weeks from current day timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_weeks "1")" +#Output +1590949626 +``` + +### date::sub_hours() + +Subtract number of hours from current day timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_hours "1")" +#Output +1591550826 +``` + +### date::sub_minutes() + +Subtract number of minutes from current day timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_minutes "1")" +#Output +1591554366 +``` + +### date::sub_seconds() + +Subtract number of seconds from current day timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_seconds "1")" +#Output +1591554425 +``` + +### date::format() + +Format unix timestamp to human readable format. +If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (string): format control characters based on `date` command (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate time string. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted time string. + +#### Example + +```bash +echo echo "$(date::format "1594143480")" +#Output +2020-07-07 18:38:00 +``` + +## Debug + +Functions to facilitate debugging scripts. + +### debug::print_array() + +Prints the content of array as key value pair for easier debugging. +Pass the variable name of the array instead of value of the variable. + +#### Arguments + +- **$1** (string): variable name of the array. + +#### Output on stdout + +- Formatted key value of array. + +#### Example + +```bash +array=(foo bar baz) +printf "Array\n" +printarr "array" +declare -A assoc_array +assoc_array=([foo]=bar [baz]=foobar) +printf "Assoc Array\n" +printarr "assoc_array" +#Output +Array +0 = foo +1 = bar +2 = baz +Assoc Array +baz = foobar +foo = bar +``` + +### debug::print_ansi() + +Function to print ansi escape sequence as is. +This function helps debug ansi escape sequence in text by displaying the escape codes. + +#### Arguments + +- **$1** (string): input with ansi escape sequence. + +#### Output on stdout + +- Ansi escape sequence printed in output as is. + +#### Example + +```bash +txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" +debug::print_ansi "$txt" +#Output +\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m +``` + +## File + +Functions for handling files. + +### file::make_temp_file() + +Create temporary file. +Function creates temporary file with random name. The temporary file will be deleted when script finishes. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If successful. +- **1**: If failed to create temp file. + +#### Output on stdout + +- file name of temporary file created. + +#### Example + +```bash +echo "$(file::make_temp_file)" +#Output +tmp.vgftzy +``` + +### file::make_temp_dir() + +Create temporary directory. +Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. + +#### Arguments + +- **$1** (string): Temporary directory prefix +- $2 string Flag to auto remove directory on exit trap (true) + +#### Exit codes + +- **0**: If successful. +- **1**: If failed to create temp directory. +- **2**: Missing arguments. + +#### Output on stdout + +- directory name of temporary directory created. + +#### Example + +```bash +echo "$(utility::make_temp_dir)" +#Output +tmp.rtfsxy +``` + +### file::name() + +Get only the filename from string path. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- name of the file with extension. + +#### Example + +```bash +echo "$(file::name "/path/to/test.md")" +#Output +test.md +``` + +### file::basename() + +Get the basename of file from file name. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- basename of the file. + +#### Example + +```bash +echo "$(file::basename "/path/to/test.md")" +#Output +test +``` + +### file::extension() + +Get the extension of file from file name. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **1**: If no extension is found in the filename. +- **2**: Function missing arguments. + +#### Output on stdout + +- extension of the file. + +#### Example + +```bash +echo "$(file::extension "/path/to/test.md")" +#Output +md +``` + +### file::dirname() + +Get directory name from file path. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- directory path. + +#### Example + +```bash +echo "$(file::dirname "/path/to/test.md")" +#Output +/path/to +``` + +### file::full_path() + +Get absolute path of file or directory. + +#### Arguments + +- **$1** (string): relative or absolute path to file/direcotry. + +#### Exit codes + +- **0**: If successful. +- **1**: If file/directory does not exist. +- **2**: Function missing arguments. + +#### Output on stdout + +- Absolute path to file/directory. + +#### Example + +```bash +file::full_path "../path/to/file.md" +#Output +/home/labbots/docs/path/to/file.md +``` + +### file::mime_type() + +Get mime type of provided input. + +#### Arguments + +- **$1** (string): relative or absolute path to file/directory. + +#### Exit codes + +- **0**: If successful. +- **1**: If file/directory does not exist. +- **2**: Function missing arguments. +- **3**: If file or mimetype command not found in system. + +#### Output on stdout + +- mime type of file/directory. + +#### Example + +```bash +file::mime_type "../src/file.sh" +#Output +application/x-shellscript +``` + +### file::contains_text() + +Search if a given pattern is found in file. + +#### Arguments + +- **$1** (string): relative or absolute path to file/directory. +- **$2** (string): search key or regular expression. + +#### Exit codes + +- **0**: If given search parameter is found in file. +- **1**: If search paramter not found in file. +- **2**: Function missing arguments. + +#### Example + +```bash +file::contains_text "./file.sh" "^[ @[:alpha:]]*" +file::contains_text "./file.sh" "@file" +#Output +0 +``` + +## Format + +Functions to format provided input. + +### format::human_readable_seconds() + +Format seconds to human readable format. + +#### Arguments + +- **$1** (int): number of seconds. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted time string. + +#### Example + +```bash +echo "$(format::human_readable_seconds "356786")" +#Output +4 days 3 hours 6 minute(s) and 26 seconds +``` + +### format::bytes_to_human() + +Format bytes to human readable format. + +#### Arguments + +- **$1** (int): size in bytes. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted file size string. + +#### Example + +```bash +echo "$(format::bytes_to_human "2250")" +#Output +2.19 KB +``` + +### format::strip_ansi() + +Remove Ansi escape sequences from given text. + +#### Arguments + +- **$1** (string): Input text to be ansi stripped. + +#### Exit codes + +- **0**: If successful. + +#### Output on stdout + +- Ansi stripped text. + +#### Example + +```bash +format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" +#Output +This is bold red text.This is green text. +``` + +### format::text_center() + +Prints the given text to centre of terminal. + +#### Arguments + +- **$1** (string): Text to be printed. +- **$2** (string): Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted text. + +#### Example + +```bash +format::text_center "This text is in centre of the terminal." "-" +``` + +### format::report() + +Format String to print beautiful report. + +#### Arguments + +- **$1** (string): Text to be printed on the left. +- **$2** (string): Text to be printed within the square brackets. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted text. + +#### Example + +```bash +format::report "Initialising mission state" "Success" +#Output +Initialising mission state ....................................................................[ Success ] +``` + +### format::trim_text_to_term() + +Trim given text to width of the terminal window. + +#### Arguments + +- **$1** (string): Text of first sentence. +- **$2** (string): Text of second sentence (optional). + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- trimmed text. + +#### Example + +```bash +format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." +#Output +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. +``` + +## Interaction + +Functions to enable interaction with the user. + +### interaction::prompt_yes_no() + +Prompt yes or no question to the user. + +#### Arguments + +- **$1** (string): The question to be prompted to the user. +- **$2** (string): default answer \[yes/no\] (optional). + +#### Exit codes + +- **0**: If user responds with yes. +- **1**: If user responds with no. +- **2**: Function missing arguments. + +#### Example + +```bash +interaction::prompt_yes_no "Are you sure to proceed" "yes" +#Output +Are you sure to proceed (y/n)? [y] +``` + +### interaction::prompt_response() + +Prompt question to the user. + +#### Arguments + +- **$1** (string): The question to be prompted to the user. +- **$2** (string): default answer (optional). + +#### Exit codes + +- **0**: If user responds with answer. +- **2**: Function missing arguments. + +#### Output on stdout + +- User entered answer to the question. + +#### Example + +```bash +interaction::prompt_response "Choose directory to install" "/home/path" +#Output +Choose directory to install? [/home/path] +``` + +## Json + +Simple json manipulation. These functions does not completely replace `jq` in any way. + +### json::get_value() + +Extract value from json based on key and position. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): id of the field to fetch. +- **$2** (int): position of value to extract.Defaults to 1.(optional) + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- string value of extracted key. + +#### Example + +```bash +json::get_value "id" "1" < json_file +json::get_value "id" <<< "${json_var}" +echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" +``` + +## Miscellaneous + +Set of miscellaneous helper functions. + +### misc::check_internet_connection() + +Check if internet connection is available. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script can connect to internet. +- **1**: If script cannot access internet. + +#### Example + +```bash +misc::check_internet_connection +``` + +### misc::get_pid() + +Get list of process ids based on process name. + +#### Arguments + +- **$1** (Name): of the process to search. + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- list of process ids. + +#### Example + +```bash +misc::get_pid "chrome" +#Ouput +25951 +26043 +26528 +26561 +``` + +### misc::get_uid() + +Get user id based on username. + +#### Arguments + +- **$1** (username): to search. + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- string uid for the username. + +#### Example + +```bash +misc::get_uid "labbots" +#Ouput +1000 +``` + +### misc::generate_uuid() + +Generate random uuid. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If match successful. + +#### Output on stdout + +- random generated uuid. + +#### Example + +```bash +misc::generate_uuid +#Ouput +65bc64d1-d355-4ffc-a9d9-dc4f3954c34c +``` + +## Operating System + +Functions to detect Operating system and version. + +### os::detect_os() + +Identify the OS the function is run on. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If OS is successfully detected. +- **1**: If unable to detect OS. + +#### Output on stdout + +- Operating system name (linux, mac or windows). + +#### Example + +```bash +os::detect_os +#Output +linux +``` + +### os::detect_linux_distro() + +Identify the distribution flavour of linux. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If Linux distro is successfully detected. +- **1**: If unable to detect OS distro. + +#### Output on stdout + +- Linux OS distribution name (ubuntu, debian, suse, etc.,). + +#### Example + +```bash +os::detect_linux_distro +#Output +ubuntu +``` + +### os::detect_linux_version() + +Identify the Linux version. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If Linux version is successfully detected. +- **1**: If unable to detect Linux version. + +#### Output on stdout + +- Linux OS version number (18.04, 20.04, etc.,). + +#### Example + +```bash +os::detect_linux_version +#Output +20.04 +``` + +### os::detect_mac_version() + +Identify the MacOS version. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If MacOS version is successfully detected. +- **1**: If unable to detect MacOS version. + +#### Output on stdout + +- MacOS version number (10.15.6, etc.,) + +#### Example + +```bash +os::detect_linux_version +#Output +10.15.7 +``` + +## String + +Functions for string operations and manipulations. + +### string::trim() + +Strip whitespace from the beginning and end of a string. + +#### Arguments + +- **$1** (string): The string to be trimmed. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- The trimmed string. + +#### Example + +```bash +echo "$(string::trim " Hello World! ")" +#Output +Hello World! +``` + +### string::split() + +Split a string to array by a delimiter. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The delimiter string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns an array of strings created by splitting the string parameter by the delimiter. + +#### Example + +```bash +array=( $(string::split "a,b,c" ",") ) +printf "%s" "$(string::split "Hello!World" "!")" +#Output +Hello +World +``` + +### string::lstrip() + +Strip characters from the beginning of a string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The characters you want to strip. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the modified string. + +#### Example + +```bash +echo "$(string::lstrip "Hello World!" "He")" +#Output +llo World! +``` + +### string::rstrip() + +Strip characters from the end of a string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The characters you want to strip. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the modified string. + +#### Example + +```bash +echo "$(string::rstrip "Hello World!" "d!")" +#Output +Hello Worl +``` + +### string::to_lower() + +Make a string lowercase. + +#### Arguments + +- **$1** (string): The input string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the lowercased string. + +#### Example + +```bash +echo "$(string::to_lower "HellO")" +#Output +hello +``` + +### string::to_upper() + +Make a string all uppercase. + +#### Arguments + +- **$1** (string): The input string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the uppercased string. + +#### Example + +```bash +echo "$(string::to_upper "HellO")" +#Output +HELLO +``` + +### string::contains() + +Check whether the search string exists within the input string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::contains "Hello World!" "lo" +``` + +### string::starts_with() + +Check whether the input string starts with key string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::starts_with "Hello World!" "He" +``` + +### string::ends_with() + +Check whether the input string ends with key string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::ends_with "Hello World!" "d!" +``` + +### string::regex() + +Check whether the input string matches the given regex. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::regex "HELLO" "^[A-Z]*$" +``` + +## Terminal + +Set of useful terminal functions. + +### terminal::is_term() + +Check if script is run in terminal. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script is run on terminal. +- **1**: If script is not run on terminal. + +### terminal::detect_profile() + +Detect profile rc file for zsh and bash of current script running user. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script is run on terminal. +- **1**: If script is not run on terminal. + +#### Output on stdout + +- path to the profile file. + +### terminal::clear_line() + +Clear the output in terminal on the specified line number. +This function clears line only on terminal. + +#### Arguments + +- **$1** (Line): number to clear. Defaults to 1. (optional) + +#### Exit codes + +- **0**: If script is run on terminal. + +#### Output on stdout + +- clear line ansi code. + +## Validation + +Functions to perform validation on given data. + +### validation::email() + +Validate whether a given input is a valid email address or not. + +#### Arguments + +- **$1** (string): input email address to validate. + +#### Exit codes + +- **0**: If provided input is an email address. +- **1**: If provided input is not an email address. +- **2**: Function missing arguments. + +#### Example + +```bash +test='test@gmail.com' +validation::email "${test}" +echo $? +#Output +0 +``` + +### validation::ipv4() + +Validate whether a given input is a valid IP V4 address. + +#### Arguments + +- **$1** (string): input IPv4 address. + +#### Exit codes + +- **0**: If provided input is a valid IPv4. +- **1**: If provided input is not a valid IPv4. +- **2**: Function missing arguments. + +#### Example + +```bash +ips=' + 4.2.2.2 + a.b.c.d + 192.168.1.1 + 0.0.0.0 + 255.255.255.255 + 255.255.255.256 + 192.168.0.1 + 192.168.0 + 1234.123.123.123 + 0.192.168.1 + ' +for ip in $ips; do + if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi + printf "%-20s: %s\n" "$ip" "$stat" +done +#Output +4.2.2.2 : good +a.b.c.d : bad +192.168.1.1 : good +0.0.0.0 : good +255.255.255.255 : good +255.255.255.256 : bad +192.168.0.1 : good +192.168.0 : bad +1234.123.123.123 : bad +0.192.168.1 : good +``` + +### validation::ipv6() + +Validate whether a given input is a valid IP V6 address. + +#### Arguments + +- **$1** (string): input IPv6 address. + +#### Exit codes + +- **0**: If provided input is a valid IPv6. +- **1**: If provided input is not a valid IPv6. +- **2**: Function missing arguments. + +#### Example + +```bash +ips=' + 2001:db8:85a3:8d3:1319:8a2e:370:7348 + fe80::1ff:fe23:4567:890a + fe80::1ff:fe23:4567:890a%eth2 + 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar + fezy::1ff:fe23:4567:890a + :: + 2001:db8:: + ' +for ip in $ips; do + if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi + printf "%-50s= %s\n" "$ip" "$stat" +done +#Output +2001:db8:85a3:8d3:1319:8a2e:370:7348 = good +fe80::1ff:fe23:4567:890a = good +fe80::1ff:fe23:4567:890a%eth2 = good +2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad +fezy::1ff:fe23:4567:890a = bad +:: = good +2001:db8:: = good +``` + +### validation::alpha() + +Validate if given variable is entirely alphabetic characters. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is only alpha characters. +- **1**: If input contains any non alpha characters. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abcABC' +validation::alpha "${test}" +echo $? +#Output +0 +``` + +### validation::alpha_num() + +Check if given variable contains only alpha-numeric characters. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is an alpha-numeric. +- **1**: If input is not an alpha-numeric. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abc123' +validation::alpha_num "${test}" +echo $? +#Output +0 +``` + +### validation::alpha_dash() + +Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is valid. +- **1**: If input the input is not valid. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abc-ABC_cD' +validation::alpha_dash "${test}" +echo $? +#Output +0 +``` + +### validation::version_comparison() + +Compares version numbers and provides return based on whether the value in equal, less than or greater. + +#### Arguments + +- **$1** (string): Version number to check (eg: 1.0.1) + +#### Exit codes + +- **0**: version number is equal. +- **1**: $1 version number is greater than $2. +- **2**: $1 version number is less than $2. +- **3**: Function is missing required arguments. +- **4**: Provided input argument is in invalid format. + +#### Example + +```bash +test='abc-ABC_cD' +validation::version_comparison "12.0.1" "12.0.1" +echo $? +#Output +0 +``` + +## Variable + +Functions for handling variables. + +### variable::is_array() + +Check if given variable is array. +Pass the variable name instead of value of the variable. + +#### Arguments + +- **$1** (string): name of the variable to check. + +#### Exit codes + +- **0**: If input is array. +- **1**: If input is not an array. + +#### Example + +```bash +arr=("a" "b" "c") +variable::is_array "arr" +#Output +0 +``` + +### variable::is_numeric() + +Check if given variable is a number. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is number. +- **1**: If input is not a number. + +#### Example + +```bash +variable::is_numeric "1234" +#Output +0 +``` + +### variable::is_int() + +Check if given variable is an integer. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is an integer. +- **1**: If input is not an integer. + +#### Example + +```bash +variable::is_int "+1234" +#Output +0 +``` + +### variable::is_float() + +Check if given variable is a float. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is a float. +- **1**: If input is not a float. + +#### Example + +```bash +variable::is_float "+1234.0" +#Output +0 +``` + +### variable::is_bool() + +Check if given variable is a boolean. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is a boolean. +- **1**: If input is not a boolean. + +#### Example + +```bash +variable::is_bool "true" +#Output +0 +``` + +### variable::is_true() + +Check if given variable is a true. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is true. +- **1**: If input is not true. + +#### Example + +```bash +variable::is_true "true" +#Output +0 +``` + +### variable::is_false() + +Check if given variable is false. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is false. +- **1**: If input is not false. + +#### Example + +```bash +variable::is_false "false" +#Output +0 +``` + +### variable::is_empty_or_null() + +Check if given variable is empty or null. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is empty or null. +- **1**: If input is not empty or null. + +#### Example + +```bash +test='' +variable::is_empty_or_null $test +#Output +0 +``` + + + +## Inspired By + +- [Bash Bible](https://github.com/dylanaraps/pure-bash-bible) - A collection of pure bash alternatives to external processes. + +## License + +[MIT](https://github.com/labbots/google-drive-upload/blob/master/LICENSE) diff --git a/src/configng/functions/bash-utility-master/bash_utility.sh b/src/configng/functions/bash-utility-master/bash_utility.sh new file mode 100644 index 000000000..65411add0 --- /dev/null +++ b/src/configng/functions/bash-utility-master/bash_utility.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +source src/array.sh +source src/string.sh +source src/variable.sh +source src/file.sh +source src/misc.sh +source src/date.sh +source src/interaction.sh +source src/check.sh +source src/format.sh +source src/collection.sh +source src/json.sh +source src/terminal.sh +source src/validation.sh +source src/debug.sh +source src/os.sh + + diff --git a/src/configng/functions/bash-utility-master/bin/bashdoc.awk b/src/configng/functions/bash-utility-master/bin/bashdoc.awk new file mode 100644 index 000000000..13efdaf7b --- /dev/null +++ b/src/configng/functions/bash-utility-master/bin/bashdoc.awk @@ -0,0 +1,275 @@ +#!/usr/bin/awk -f + +# Varibles +# style = readme or doc +# toc = true or false +BEGIN { + if (! style) { + style = "doc" + } + + if (! toc) { + toc = 0 + } + + styles["empty", "from"] = ".*" + styles["empty", "to"] = "" + + styles["h1", "from"] = ".*" + styles["h1", "to"] = "# &" + + styles["h2", "from"] = ".*" + styles["h2", "to"] = "## &" + + styles["h3", "from"] = ".*" + styles["h3", "to"] = "### &" + + styles["h4", "from"] = ".*" + styles["h4", "to"] = "#### &" + + styles["h5", "from"] = ".*" + styles["h5", "to"] = "##### &" + + styles["code", "from"] = ".*" + styles["code", "to"] = "```&" + + styles["/code", "to"] = "```" + + styles["argN", "from"] = "^(\\$[0-9]) (\\S+)" + styles["argN", "to"] = "**\\1** (\\2):" + + styles["arg@", "from"] = "^\\$@ (\\S+)" + styles["arg@", "to"] = "**...** (\\1):" + + styles["li", "from"] = ".*" + styles["li", "to"] = "- &" + + styles["i", "from"] = ".*" + styles["i", "to"] = "*&*" + + styles["anchor", "from"] = ".*" + styles["anchor", "to"] = "[&](#&)" + + styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" + styles["exitcode", "to"] = "**\\1**: \\2" + + styles["h_rule", "to"] = "---" + + styles["comment", "from"] = ".*" + styles["comment", "to"] = "" + + + output_format["readme", "h1"] = "h2" + output_format["readme", "h2"] = "h3" + output_format["readme", "h3"] = "h4" + output_format["readme", "h4"] = "h5" + + output_format["bashdoc", "h1"] = "h1" + output_format["bashdoc", "h2"] = "h2" + output_format["bashdoc", "h3"] = "h3" + output_format["bashdoc", "h4"] = "h4" + + output_format["webdoc", "h1"] = "empty" + output_format["webdoc", "h2"] = "h3" + output_format["webdoc", "h3"] = "h4" + output_format["webdoc", "h4"] = "h5" + +} + +function render(type, text) { + if((style,type) in output_format){ + type = output_format[style,type] + } + return gensub( \ + styles[type, "from"], + styles[type, "to"], + "g", + text \ + ) +} + +function render_list(item, anchor) { + return "- [" item "](#" anchor ")" +} + +function generate_anchor(text) { + # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 + text = tolower(text) + gsub(/[^[:alnum:]_ -]/, "", text) + gsub(/ /, "-", text) + return text +} + +function reset() { + has_example = 0 + has_args = 0 + has_exitcode = 0 + has_stdout = 0 + + content_desc = "" + content_example = "" + content_args = "" + content_exitcode = "" + content_seealso = "" + content_stdout = "" +} + +/^[[:space:]]*# @internal/ { + is_internal = 1 +} + +/^[[:space:]]*# @file/ { + sub(/^[[:space:]]*# @file /, "") + + filedoc = render("h1", $0) "\n" + if(style == "webdoc"){ + filedoc = filedoc render("comment", "file=" $0) "\n" + } + +} + +/^[[:space:]]*# @brief/ { + sub(/^[[:space:]]*# @brief /, "") + if(style == "webdoc"){ + filedoc = filedoc render("comment", "brief=" $0) "\n" + } + filedoc = filedoc "\n" $0 +} + +/^[[:space:]]*# @description/ { + in_description = 1 + in_example = 0 + + reset() + + docblock = "" +} + +in_description { + if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) { + if (!match(content_desc, /\n$/)) { + content_desc = content_desc "\n" + } + in_description = 0 + } else { + sub(/^[[:space:]]*# @description /, "") + sub(/^[[:space:]]*# /, "") + sub(/^[[:space:]]*#$/, "") + + content_desc = content_desc "\n" $0 + } +} + +in_example { + + if (! /^[[:space:]]*#[ ]{3}/) { + + in_example = 0 + + content_example = content_example "\n" render("/code") "\n" + } else { + sub(/^[[:space:]]*#[ ]{3}/, "") + + content_example = content_example "\n" $0 + } +} + +/^[[:space:]]*# @example/ { + in_example = 1 + content_example = content_example "\n" render("h3", "Example") + content_example = content_example "\n\n" render("code", "bash") +} + +/^[[:space:]]*# @arg/ { + if (!has_args) { + has_args = 1 + + content_args = content_args "\n" render("h3", "Arguments") "\n\n" + } + + sub(/^[[:space:]]*# @arg /, "") + + $0 = render("argN", $0) + $0 = render("arg@", $0) + + content_args = content_args render("li", $0) "\n" +} + +/^[[:space:]]*# @noargs/ { + content_args = content_args "\n" render("i", "Function has no arguments.") "\n" +} + +/^[[:space:]]*# @exitcode/ { + if (!has_exitcode) { + has_exitcode = 1 + + content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n" + } + + sub(/^[[:space:]]*# @exitcode /, "") + + $0 = render("exitcode", $0) + + content_exitcode = content_exitcode render("li", $0) "\n" +} + +/^[[:space:]]*# @see/ { + sub(/[[:space:]]*# @see /, "") + anchor = generate_anchor($0) + $0 = render_list($0, anchor) + + content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n" +} + +/^[[:space:]]*# @stdout/ { + has_stdout = 1 + + sub(/^[[:space:]]*# @stdout /, "") + + content_stdout = content_stdout "\n" render("h3", "Output on stdout") + content_stdout = content_stdout "\n\n" render("li", $0) "\n" +} + +{ + docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso + if(style == "webdoc"){ + docblock = docblock "\n" render("h_rule") "\n" + } +} + +/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example { + if (is_internal) { + is_internal = 0 + } else { + func_name = gensub(\ + /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \ + "\\3()", \ + "g" \ + ) + doc = doc "\n" render("h2", func_name) "\n" docblock + if (toc) { + url = generate_anchor(func_name) + + content_idx = content_idx "\n" "- [" func_name "](#" url ")" + } + } + + docblock = "" + reset() +} + +END { + if (filedoc != "") { + print filedoc + } + + if (toc) { + print "" + print render("h2", "Table of Contents") + print content_idx + print "" + print render("h_rule") + } + + print doc +} diff --git a/src/configng/functions/bash-utility-master/bin/generate_readme.sh b/src/configng/functions/bash-utility-master/bin/generate_readme.sh new file mode 100644 index 000000000..858a4b8be --- /dev/null +++ b/src/configng/functions/bash-utility-master/bin/generate_readme.sh @@ -0,0 +1,400 @@ +#!/usr/bin/env bash + +#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh +#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh +_usage() { + printf " +Script to autogenerate markdown based on bash source code.\n +The script generates table of contents and bashdoc and update the given markdown file.\n +Usage:\n %s [options.. ]\n +Options:\n + -f | --file - Relative or absolute path to the README.md file. + -s | --sh-dir - path to the bash script source folder to generate shdocs.\n + -l | --toc-level - Minimum level of header to print in Table of Contents.\n + -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n + -w | --webdoc - Flag to indicate generation of webdoc.\n + -p | --dest-dir - Path in which wedoc files must be generated.\n + -h | --help - Display usage instructions.\n" "${0##*/}" + exit 0 +} + +_setup_arguments() { + + unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR + MINLEVEL=1 + MAXLEVEL=3 + SCRIPT_FILE="${0##*/}" + declare source="${BASH_SOURCE[0]}" + while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink + SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" + source="$(readlink "$source")" + [[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located + done + SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" + SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md" + SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src" + WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions" + + SHORTOPTS="whp:f:m:d:s:-:" + + while getopts "${SHORTOPTS}" OPTION; do + case "${OPTION}" in + -) + _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } + case "${OPTARG}" in + help) + _usage + ;; + file) + _check_longoptions "${!OPTIND}" + SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-level) + _check_longoptions "${!OPTIND}" + MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-depth) + _check_longoptions "${!OPTIND}" + MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + sh-dir) + _check_longoptions "${!OPTIND}" + SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + webdoc) + WEBDOC=true + ;; + dest-dir) + WEBDOC_DEST_DIR="${OPTARG}" + ;; + '') + _usage + ;; + *) + printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + ;; + h) + _usage + ;; + f) + SOURCE_MARKDOWN="${OPTARG}" + ;; + m) + MINLEVEL="${OPTARG}" + ;; + d) + MAXLEVEL="${OPTARG}" + ;; + s) + SOURCE_SCRIPT_DIR="${OPTARG}" + ;; + w) + WEBDOC=true + ;; + p) + WEBDOC_DEST_DIR="${OPTARG}" + ;; + :) + printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + ?) + printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + done + shift "$((OPTIND - 1))" + + if [[ -w "${SOURCE_MARKDOWN}" ]]; then + declare src_file src_extension + src_file="${SOURCE_MARKDOWN##*/}" + src_extension="${src_file##*.}" + if [[ "${src_extension,,}" != "md" ]]; then + printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 + fi + else + printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 + fi + + if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then + printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 + fi + + declare re='^[0-9]+$' + if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then + echo "error: Not a number" >&2 + exit 1 + fi + if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then + printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 + fi + + [ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}" + +} + +_setup_tempfile() { + declare temp_file + type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } + trap 'rm -f "${temp_file}"' EXIT + printf "%s" "${temp_file}" +} + +_generate_shdoc() { + declare file + file="$(realpath "${1}")" + if [[ -s "${file}" ]]; then + awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2" + #awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md" + fi +} + +_insert_shdoc_to_file() { + declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc + shdoc_tmp_file="$1" + source_markdown="$2" + + start_shdoc="" + info_shdoc="" + end_shdoc="" + + sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" + + if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then + # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file + + sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" + echo -e "Updated bashdoc content to ${source_markdown} successfully\n" + + else + { + printf "%s\n" "${start_shdoc}" + cat "${shdoc_tmp_file}" + printf "%s\n" "${end_shdoc}" + } >> "${source_markdown}" + echo -e "Created bashdoc content to ${source_markdown} successfully\n" + fi +} + +_process_sh_files() { + declare shdoc_tmp_file source_script_dir source_markdown + source_markdown="${1}" + source_script_dir="${2}" + shdoc_tmp_file=$(_setup_tempfile) + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + while IFS= read -r -d '' line; do + _generate_shdoc "${line}" "${shdoc_tmp_file}" + done + _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" + rm "${shdoc_tmp_file}" + +} + +_generate_toc() { + + declare line level title anchor output counter temp_output invalid_chars + + invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" + while IFS='' read -r line || [[ -n "${line}" ]]; do + level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" + title="$(echo "${line}" | sed -E 's/^#+ //')" + [[ "${title}" = "Table of Contents" ]] && continue + + # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text + anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" + + # check new line introduced is not duplicated, if is duplicated, introduce a number at the end + temp_output=$output"$level- [$title](#$anchor)\n" + counter=1 + while true; do + nlines="$(echo -e "${temp_output}" | wc -l)" + duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)" + if [ "${nlines}" = "${duplines}" ]; then + break + fi + temp_output=$output"$level- [$title](#$anchor-$counter)\n" + counter=$((counter + 1)) + done + + output="$temp_output" + + # grep: filter header candidates to be included in toc + # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment + done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")" + + # when in toc we have two `--` quit one + echo "$output" + +} + +_insert_toc_to_file() { + + declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash + source_markdown="${1}" + toc_text="${2}" + start_toc="" + info_toc="" + end_toc="" + + toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" + # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command + utext_ampersand="id8234923000230gzz" + utext_slash="id9992384923423gzz" + toc_block="${toc_block//\&/${utext_ampersand}}" + toc_block="${toc_block//\//${utext_slash}}" + + # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 + # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches + if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then + # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace + sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" + echo -e "Updated TOC content in ${source_markdown} succesfully\n" + + else + sed -i 1i"$toc_block" "${source_markdown}" + echo -e "Created TOC in ${source_markdown} succesfully\n" + + fi + + # undo symbol replacements + sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" + sed -i "s,${utext_slash},\/,g" "${source_markdown}" + +} + +_process_toc() { + declare toc_temp_file source_markdown level toc_text + source_markdown="${1}" + + toc_temp_file=$(_setup_tempfile) + + sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" + + level=$MINLEVEL + while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do + level=$((level + 1)) + done + + MINLEVEL=${level} + toc_text=$(_generate_toc "${toc_temp_file}") + rm "${toc_temp_file}" + + _insert_toc_to_file "${source_markdown}" "${toc_text}" +} + +_generate_webdoc() { + declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file + declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc + declare webdoc_lastmod_date webdoc_lastmod_epoc + file="$(realpath "${1}")" + dest_dir="${2}" + + filename="${file##*/}" + file_basename="${filename%.*}" + dest_file_path="${dest_dir}/${file_basename}.md" + file_modified_date="$(date -r "${file}" +"%FT%T%:z")" + file_modified_date_epoc="$(date -r "${file}" +"%s")" + + start_shdoc="" + end_shdoc="" + if [[ ! -f "${dest_file_path}" ]]; then + + cat << EOF > "${dest_file_path}" +--- +title : +description : +date : ${file_modified_date} +lastmod : ${file_modified_date} +--- +${start_shdoc} +${end_shdoc} +EOF + is_new_file=true + else + is_new_file=false + webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")" + webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")" + fi + + if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then + + shdoc_tmp_file=$(_setup_tempfile) + if [[ -s "${file}" ]]; then + awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}" + fi + + if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then + sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}" + fi + + # Extract title and description from webdoc + title="$(sed -ne 's/-->//; s/^.*//; s/^.*/${title}/g" "${dest_file_path}" + sed -i -e "s//${description}/g" "${dest_file_path}" + else + sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}" + sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}" + + fi + + # Update the last modified timestamp in front matter + sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}" + + echo -e "Updated bashdoc content to ${dest_file_path} successfully." + rm "${shdoc_tmp_file}" + + fi +} +_process_webdoc_files() { + declare source_script_dir dest_dir + + source_script_dir="${1}" + dest_dir="${2}" + + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + while IFS= read -r -d '' line; do + _generate_webdoc "${line}" "${dest_dir}" + done +} + +_count_library_functions() { + declare source_script_dir + source_script_dir="${1}" + + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + { + declare -i function_count=0 count=0 + while IFS= read -r -d '' line; do + count=0 + count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}") + function_count=$((function_count + count)) + done + printf "Total library functions: %s \n" "${function_count}" + + } +} +main() { + # export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' + # set -x + trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT + set -o errexit -o noclobber -o pipefail + + _setup_arguments "${@}" + _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" + _process_toc "${SOURCE_MARKDOWN}" + + if [[ -n ${WEBDOC} ]]; then + _process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}" + fi + _count_library_functions "${SOURCE_SCRIPT_DIR}" +} + +main "${@}" diff --git a/src/configng/functions/bash-utility-master/image/bash-utility.png b/src/configng/functions/bash-utility-master/image/bash-utility.png new file mode 100644 index 0000000000000000000000000000000000000000..c1bfb8b556809ce645cf41ab6d4b11547df68fc7 GIT binary patch literal 32396 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 literal 0 HcmV?d00001 diff --git a/src/configng/functions/bash-utility-master/src/array.sh b/src/configng/functions/bash-utility-master/src/array.sh new file mode 100644 index 000000000..42d5883b8 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/array.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash + +# @file Array +# @brief Functions for array operations and manipulations. + +# @description Check if item exists in the given array. +# +# @example +# array=("a" "b" "c") +# array::contains "c" ${array[@]} +# #Output +# 0 +# +# @arg $1 mixed Item to search (needle). +# @arg $2 array array to be searched (haystack). +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found in the array. +# @exitcode 2 Function missing arguments. +array::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare query="${1:-}" + shift + + for element in "${@}"; do + [[ "${element}" == "${query}" ]] && return 0 + done + + return 1 +} + +# @description Remove duplicate items from the array. +# +# @example +# array=("a" "b" "a" "c") +# printf "%s" "$(array::dedupe ${array[@]})" +# #Output +# a +# b +# c +# +# @arg $1 array Array to be deduped. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Deduplicated array. +array::dedupe() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -A arr_tmp + declare -a arr_unique + for i in "$@"; do + { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue + arr_unique+=("${i}") && arr_tmp[${i}]=x + done + printf '%s\n' "${arr_unique[@]}" +} + +# @description Check if a given array is empty. +# +# @example +# array=("a" "b" "c" "d") +# array::is_empty "${array[@]}" +# +# @arg $1 array Array to be checked. +# +# @exitcode 0 If the given array is empty. +# @exitcode 2 If the given array is not empty. +array::is_empty() { + declare -a array + local array=("$@") + if [ ${#array[@]} -eq 0 ]; then + return 0 + else + return 1 + fi +} +# @description Join array elements with a string. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s" "$(array::join "," "${array[@]}")" +# #Output +# a,b,c,d +# printf "%s" "$(array::join "" "${array[@]}")" +# #Output +# abcd +# +# @arg $1 string String to join the array elements (glue). +# @arg $2 array array to be joined with glue string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. +array::join() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare delimiter="${1}" + shift + printf "%s" "${1}" + shift + printf "%s" "${@/#/${delimiter}}" +} + +# @description Return an array with elements in reverse order. +# +# @example +# array=(1 2 3 4 5) +# printf "%s" "$(array::reverse "${array[@]}")" +# #Output +# 5 4 3 2 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The reversed array. +array::reverse() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare min=0 + declare -a array + array=("$@") + declare max=$((${#array[@]} - 1)) + + while [[ $min -lt $max ]]; do + # Swap current first and last elements + x="${array[$min]}" + array[$min]="${array[$max]}" + array[$max]="$x" + + # Move closer + ((min++, max--)) + done + printf '%s\n' "${array[@]}" +} + +# @description Returns a random item from the array. +# +# @example +# array=("a" "b" "c" "d") +# printf "%s\n" "$(array::random_element "${array[@]}")" +# #Output +# c +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Random item out of the array. +array::random_element() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array + local array=("$@") + printf '%s\n' "${array[RANDOM % $#]}" +} + +# @description Sort an array from lowest to highest. +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# 1 +# 2 +# 4 5 +# a +# a c +# d +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout sorted array. +array::sort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort <<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Sort an array in reverse order (highest to lowest). +# +# @example +# sarr=("a c" "a" "d" 2 1 "4 5") +# array::array_sort "${sarr[@]}" +# #Output +# d +# a c +# a +# 4 5 +# 2 +# 1 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout reverse sorted array. +array::rsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a array=("$@") + declare -a sorted + declare noglobtate + noglobtate="$(shopt -po noglob)" + set -o noglob + declare IFS=$'\n' + sorted=($(sort -r<<< "${array[*]}")) + unset IFS + eval "${noglobtate}" + printf "%s\n" "${sorted[@]}" +} + +# @description Bubble sort an integer array from lowest to highest. +# This sort does not work on string array. +# @example +# iarr=(4 5 1 3) +# array::bsort "${iarr[@]}" +# #Output +# 1 +# 3 +# 4 +# 5 +# +# @arg $1 array The input array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout bubble sorted array. +array::bsort() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare tmp + declare arr=("$@") + for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do + for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do + if [[ ${arr[i]} -gt ${arr[j]} ]]; then + # echo $i $j ${arr[i]} ${arr[j]} + tmp=${arr[i]} + arr[i]=${arr[j]} + arr[j]=$tmp + fi + done + done + printf "%s\n" "${arr[@]}" +} + +# @description Merge two arrays. +# Pass the variable name of the array instead of value of the variable. +# @example +# a=("a" "c") +# b=("d" "c") +# array::merge "a[@]" "b[@]" +# #Output +# a +# c +# d +# c +# +# @arg $1 string variable name of first array. +# @arg $2 string variable name of second array. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Merged array. +array::merge() { + [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr1=("${!1}") + declare -a arr2=("${!2}") + declare out=("${arr1[@]}" "${arr2[@]}") + printf "%s\n" "${out[@]}" +} diff --git a/src/configng/functions/bash-utility-master/src/check.sh b/src/configng/functions/bash-utility-master/src/check.sh new file mode 100644 index 000000000..2b7c1eb1d --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/check.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# @file Check +# @brief Helper functions. + +# @description Check if the command exists in the system. +# +# @example +# check::command_exists "tput" +# +# @arg $1 string Command name to be searched. +# +# @exitcode 0 If the command exists. +# @exitcode 1 If the command does not exist. +# @exitcode 2 Function missing arguments. +check::command_exists() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + hash "${1}" 2> /dev/null +} + +# @description Check if the script is executed with sudo privilege. +# +# @example +# check::is_sudo +# +# @noargs +# +# @exitcode 0 If the script is executed with root privilege. +# @exitcode 1 If the script is not executed with root privilege +check::is_sudo() { + if [[ $(id -u) -ne 0 ]]; then + return 1 + fi +} diff --git a/src/configng/functions/bash-utility-master/src/collection.sh b/src/configng/functions/bash-utility-master/src/collection.sh new file mode 100644 index 000000000..9f0e244a2 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/collection.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash + +# @file Collection +# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. + +# @description Iterates over elements of collection and invokes iteratee for each element. +# Input to the function can be a pipe output, here-string or file. +# @example +# test_func(){ +# printf "print value: %s\n" "$1" +# return 0 +# } +# arr1=("a b" "c d" "a" "d") +# printf "%s\n" "${arr1[@]}" | collection::each "test_func" +# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach +# #output +# print value: a b +# print value: c d +# print value: a +# print value: d +# +# @example +# # If other function from this library is already used to process the array. +# # Then following method could be used to pass the array to the function. +# out=("$(array::dedupe "${arr1[@]}")") +# collection::each "test_func" <<< "${out[@]}" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output of iteratee function. +collection::each() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + done +} + +# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "4") +# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If iteratee function fails. +# @exitcode 2 Function missing arguments. +collection::every() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + + if [[ $ret -ne 0 ]]; then + return 1 + fi + + done +} + +# @description Iterates over elements of array, returning all elements where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" +# #output +# 1 +# 2 +# 3 +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values matching the iteratee function. +collection::filter() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s\n" "${it}" + fi + done +} + +# @description Iterates over elements of collection, returning the first element where iteratee returns true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("1" "2" "3" "a") +# check_a(){ +# [[ "$1" = "a" ]] +# } +# printf "%s\n" "${arr[@]}" | collection::find "check_a" +# #Output +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +# +# @stdout first array value matching the iteratee function. +collection::find() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'${it}'" + fi + declare -i ret="$?" + if [[ $ret = 0 ]]; then + printf "%s" "${it}" + return 0 + fi + done + + return 1 +} + +# @description Invokes the iteratee with each element passed as argument to the iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# opt=("-a" "-l") +# printf "%s\n" "${opt[@]}" | collection::invoke "ls" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output from the iteratee function. +collection::invoke() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a args=() + declare func="${1}" + while read -r it; do + args=("${args[@]}" "$it") + done + + eval "${func}" "${args[@]}" +} + +# @description Creates an array of values by running each element in array through iteratee. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3") +# add_one(){ +# i=${1} +# i=$(( i + 1 )) +# printf "%s\n" "$i" +# } +# printf "%s\n" "${arri[@]}" | collection::map "add_one" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# @exitcode other exitcode returned by iteratee. +# +# @stdout Output result of iteratee on value. +collection::map() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + declare out + + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + out="$("${func}")" + else + out="$("${func}" "$it")" + fi + + declare -i ret=$? + + if [[ $ret -ne 0 ]]; then + return $ret + fi + + printf "%s\n" "${out}" + done +} + +# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. +# Input to the function can be a pipe output, here-string or file. +# @example +# arri=("1" "2" "3" "a") +# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" +# #Ouput +# a +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout array values not matching the iteratee function. +# @see collection::filter +collection::reject() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + echo "$it" + fi + + done +} + +# @description Checks if iteratee returns true for any element of the array. +# Input to the function can be a pipe output, here-string or file. +# @example +# arr=("a" "b" "3" "a") +# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" +# +# @arg $1 string Iteratee function. +# +# @exitcode 0 If match successful. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +collection::some() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare func="${1}" + declare IFS=$'\n' + while read -r it; do + + if [[ "${func}" == *"$"* ]]; then + eval "${func}" + else + eval "${func}" "'$it'" + fi + + declare -i ret=$? + + if [[ $ret -eq 0 ]]; then + return 0 + fi + done + + return 1 +} diff --git a/src/configng/functions/bash-utility-master/src/date.sh b/src/configng/functions/bash-utility-master/src/date.sh new file mode 100644 index 000000000..41f071792 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/date.sh @@ -0,0 +1,744 @@ +#!/usr/bin/env bash + +# @file Date +# @brief Functions for manipulating dates. + +# @description Get current time in unix timestamp. +# +# @example +# echo "$(date::now)" +# #Output +# 1591554426 +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout current timestamp. +date::now() { + declare now + now="$(date --universal +%s)" || return $? + printf "%s" "${now}" +} + +# @description convert datetime string to unix timestamp. +# +# @example +# echo "$(date::epoc "2020-07-07 18:38")" +# #Output +# 1594143480 +# +# @arg $1 string date time in any format. +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp for specified datetime. +date::epoc() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare date + date=$(date -d "${1}" +"%s") || return $? + printf "%s" "${date}" +} + +# @description Add number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days_from "1594143480")" +# #Output +# 1594229880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months_from "1594143480")" +# #Output +# 1596821880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years_from "1594143480")" +# #Output +# 1625679480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::add_weeks_from "1594143480")" +# #Output +# 1594748280 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours_from "1594143480")" +# #Output +# 1594147080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes_from "1594143480")" +# #Output +# 1594143540 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds_from "1594143480")" +# #Output +# 1594143481 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::add_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::add_days "1")" +# #Output +# 1591640826 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::add_months "1")" +# #Output +# 1594146426 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_years "1")" +# #Output +# 1623090426 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 year. +# +# @example +# echo "$(date::add_weeks "1")" +# #Output +# 1592159226 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::add_hours "1")" +# #Output +# 1591558026 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::add_minutes "1")" +# #Output +# 1591554486 +# +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Add number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::add_seconds "1")" +# #Output +# 1591554427 +# +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::add_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from specified timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days_from "1594143480")" +# #Output +# 1594057080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_days_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp day + timestamp="${1}" + day=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from specified timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months_from "1594143480")" +# #Output +# 1591551480 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_months_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp month + timestamp="${1}" + month=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from specified timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years_from "1594143480")" +# #Output +# 1562521080 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_years_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp year + timestamp="${1}" + year=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from specified timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks_from "1594143480")" +# #Output +# 1593538680 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_weeks_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp week + timestamp="${1}" + week=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from specified timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours_from "1594143480")" +# #Output +# 1594139880 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_hours_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp hour + timestamp="${1}" + hour=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from specified timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes_from "1594143480")" +# #Output +# 1594143420 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_minutes_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + minute=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from specified timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds_from "1594143480")" +# #Output +# 1594143479 +# +# @arg $1 int unix timestamp. +# @arg $2 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# @exitcode 2 Function missing arguments. +# +# @stdout timestamp. +date::sub_seconds_from() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp new_timestamp minute + timestamp="${1}" + second=${2:-1} + new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of days from current day timestamp. +# If number of days not specified then it defaults to 1 day. +# +# @example +# echo "$(date::sub_days "1")" +# #Output +# 1588876026 +# +# @arg $1 int number of days (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_days() { + declare timestamp new_timestamp day + timestamp="$(date::now)" + day=${1:-1} + new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of months from current day timestamp. +# If number of months not specified then it defaults to 1 month. +# +# @example +# echo "$(date::sub_months "1")" +# #Output +# 1559932026 +# +# @arg $1 int number of months (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_months() { + declare timestamp new_timestamp month + timestamp="$(date::now)" + month=${1:-1} + new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of years from current day timestamp. +# If number of years not specified then it defaults to 1 year. +# +# @example +# echo "$(date::sub_years "1")" +# #Output +# 1591468026 +# +# @arg $1 int number of years (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_years() { + declare timestamp new_timestamp year + timestamp="$(date::now)" + year=${1:-1} + new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of weeks from current day timestamp. +# If number of weeks not specified then it defaults to 1 week. +# +# @example +# echo "$(date::sub_weeks "1")" +# #Output +# 1590949626 +# +# @arg $1 int number of weeks (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_weeks() { + declare timestamp new_timestamp week + timestamp="$(date::now)" + week=${1:-1} + new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of hours from current day timestamp. +# If number of hours not specified then it defaults to 1 hour. +# +# @example +# echo "$(date::sub_hours "1")" +# #Output +# 1591550826 +# +# @arg $1 int number of hours (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_hours() { + declare timestamp new_timestamp hour + timestamp="$(date::now)" + hour=${1:-1} + new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of minutes from current day timestamp. +# If number of minutes not specified then it defaults to 1 minute. +# +# @example +# echo "$(date::sub_minutes "1")" +# #Output +# 1591554366 +# +# @arg $1 int number of minutes (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_minutes() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + minute=${1:-1} + new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Subtract number of seconds from current day timestamp. +# If number of seconds not specified then it defaults to 1 second. +# +# @example +# echo "$(date::sub_seconds "1")" +# #Output +# 1591554425 +# +# @arg $1 int number of seconds (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate timestamp. +# +# @stdout timestamp. +date::sub_seconds() { + declare timestamp new_timestamp minute + timestamp="$(date::now)" + second=${1:-1} + new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? + printf "%s" "${new_timestamp}" +} + +# @description Format unix timestamp to human readable format. +# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. +# +# @example +# echo echo "$(date::format "1594143480")" +# #Output +# 2020-07-07 18:38:00 +# +# @arg $1 int unix timestamp. +# @arg $2 string format control characters based on `date` command (optional). +# +# @exitcode 0 If successful. +# @exitcode 1 If unable to generate time string. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +date::format() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare timestamp format out + timestamp="${1}" + format="${2:-"%F %T"}" + out="$(date -d "@${timestamp}" +"${format}")" || return $? + printf "%s" "${out}" + +} diff --git a/src/configng/functions/bash-utility-master/src/debug.sh b/src/configng/functions/bash-utility-master/src/debug.sh new file mode 100644 index 000000000..82828734f --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/debug.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# @file Debug +# @brief Functions to facilitate debugging scripts. + +# @description Prints the content of array as key value pair for easier debugging. +# Pass the variable name of the array instead of value of the variable. +# @example +# array=(foo bar baz) +# printf "Array\n" +# printarr "array" +# declare -A assoc_array +# assoc_array=([foo]=bar [baz]=foobar) +# printf "Assoc Array\n" +# printarr "assoc_array" +# #Output +# Array +# 0 = foo +# 1 = bar +# 2 = baz +# Assoc Array +# baz = foobar +# foo = bar +# +# @arg $1 string variable name of the array. +# +# @stdout Formatted key value of array. +debug::print_array() { + declare -n __arr="$1" + for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done +} + +# @description Function to print ansi escape sequence as is. +# This function helps debug ansi escape sequence in text by displaying the escape codes. +# +# @example +# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" +# debug::print_ansi "$txt" +# #Output +# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m +# +# @arg $1 string input with ansi escape sequence. +# +# @stdout Ansi escape sequence printed in output as is. +debug::print_ansi() { + #echo $(tr -dc '[:print:]'<<<$1) + printf "%s\n" "${1//$'\e'/\\e}" + +} diff --git a/src/configng/functions/bash-utility-master/src/file.sh b/src/configng/functions/bash-utility-master/src/file.sh new file mode 100644 index 000000000..71afd8476 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/file.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash + +# @file File +# @brief Functions for handling files. + +# @description Create temporary file. +# Function creates temporary file with random name. The temporary file will be deleted when script finishes. +# +# @example +# echo "$(file::make_temp_file)" +# #Output +# tmp.vgftzy +# +# @noargs +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp file. +# +# @stdout file name of temporary file created. +file::make_temp_file() { + declare temp_file + type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } + trap 'rm -f "${temp_file}"' EXIT + printf "%s" "${temp_file}" +} + +# @description Create temporary directory. +# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. +# +# @example +# echo "$(utility::make_temp_dir)" +# #Output +# tmp.rtfsxy +# +# @arg $1 string Temporary directory prefix +# @arg $2 string Flag to auto remove directory on exit trap (true) +# +# @exitcode 0 If successful. +# @exitcode 1 If failed to create temp directory. +# @exitcode 2 Missing arguments. +# +# @stdout directory name of temporary directory created. +file::make_temp_dir() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare temp_dir prefix="${1}" trap_rm="${2}" + temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") + if [[ -n "${trap_rm}" ]]; then + trap 'rm -rf "${temp_dir}"' EXIT + fi + printf "%s" "${temp_dir}" +} + +# @description Get only the filename from string path. +# +# @example +# echo "$(file::name "/path/to/test.md")" +# #Output +# test.md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout name of the file with extension. +file::name() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf "%s" "${1##*/}" +} + +# @description Get the basename of file from file name. +# +# @example +# echo "$(file::basename "/path/to/test.md")" +# #Output +# test +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout basename of the file. +file::basename() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare file basename + file="${1##*/}" + basename="${file%.*}" + + printf "%s" "${basename}" +} + +# @description Get the extension of file from file name. +# +# @example +# echo "$(file::extension "/path/to/test.md")" +# #Output +# md +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 1 If no extension is found in the filename. +# @exitcode 2 Function missing arguments. +# +# @stdout extension of the file. +file::extension() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare file extension + file="${1##*/}" + extension="${file##*.}" + [[ "${file}" = "${extension}" ]] && return 1 + + printf "%s" "${extension}" +} + +# @description Get directory name from file path. +# +# @example +# echo "$(file::dirname "/path/to/test.md")" +# #Output +# /path/to +# +# @arg $1 string path. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout directory path. +file::dirname() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare tmp=${1:-.} + + [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } + tmp="${tmp%%"${tmp##*[!/]}"}" + + [[ ${tmp} != */* ]] && { printf '.\n' && return; } + tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" + + printf '%s' "${tmp:-/}" +} + +# @description Get absolute path of file or directory. +# +# @example +# file::full_path "../path/to/file.md" +# #Output +# /home/labbots/docs/path/to/file.md +# +# @arg $1 string relative or absolute path to file/direcotry. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# +# @stdout Absolute path to file/directory. +file::full_path() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare input="${1}" + if [[ -f ${input} ]]; then + printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" + elif [[ -d ${input} ]]; then + printf "%s\n" "$(cd "${input}" && pwd)" + else + return 1 + fi +} + +# @description Get mime type of provided input. +# +# @example +# file::mime_type "../src/file.sh" +# #Output +# application/x-shellscript +# +# @arg $1 string relative or absolute path to file/directory. +# +# @exitcode 0 If successful. +# @exitcode 1 If file/directory does not exist. +# @exitcode 2 Function missing arguments. +# @exitcode 3 If file or mimetype command not found in system. +# +# @stdout mime type of file/directory. +file::mime_type() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare mime_type + if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then + if type -p mimetype &> /dev/null; then + mime_type=$(mimetype --output-format %m "${1}") + elif type -p file &> /dev/null; then + mime_type=$(file --brief --mime-type "${1}") + else + return 3 + fi + else + return 1 + fi + printf "%s" "${mime_type}" +} + +# @description Search if a given pattern is found in file. +# +# @example +# file::contains_text "./file.sh" "^[ @[:alpha:]]*" +# file::contains_text "./file.sh" "@file" +# #Output +# 0 +# +# @arg $1 string relative or absolute path to file/directory. +# @arg $2 string search key or regular expression. +# +# @exitcode 0 If given search parameter is found in file. +# @exitcode 1 If search paramter not found in file. +# @exitcode 2 Function missing arguments. +file::contains_text() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + declare -r file="$1" + declare -r text="$2" + grep -q "$text" "$file" +} diff --git a/src/configng/functions/bash-utility-master/src/format.sh b/src/configng/functions/bash-utility-master/src/format.sh new file mode 100644 index 000000000..7dceb1d59 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/format.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash + +# @file Format +# @brief Functions to format provided input. + +# @internal +# @description Initialisation script when the code is sourced. +# +# @noargs +__init(){ +_check_terminal_window_size +} + +# @internal +# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. +# +# @noargs +_check_terminal_window_size() { + shopt -s checkwinsize && (: && :) + trap 'shopt -s checkwinsize; (:;:)' SIGWINCH +} +# @description Format seconds to human readable format. +# +# @example +# echo "$(format::human_readable_seconds "356786")" +# #Output +# 4 days 3 hours 6 minute(s) and 26 seconds +# +# @arg $1 int number of seconds. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted time string. +format::human_readable_seconds() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare T="${1}" + declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" + [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" + [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" + [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" + [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' + printf '%d seconds\n' "${SEC}" +} + +# @description Format bytes to human readable format. +# +# @example +# echo "$(format::bytes_to_human "2250")" +# #Output +# 2.19 KB +# +# @arg $1 int size in bytes. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted file size string. +format::bytes_to_human() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) + while ((b > 1024)); do + d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" + b=$((b / 1024)) && ((s++)) + done + printf "%s\n" "${b}${d} ${S[${s}]}" +} + +# @description Remove Ansi escape sequences from given text. +# +# @example +# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" +# #Output +# This is bold red text.This is green text. +# +# @arg $1 string Input text to be ansi stripped. +# +# @exitcode 0 If successful. +# +# @stdout Ansi stripped text. +format::strip_ansi() { + declare tmp esc tpa re + tmp="${1}" + esc=$(printf "\x1b") + tpa=$(printf "\x28") + re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" + while [[ "${tmp}" =~ $re ]]; do + tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" + done + printf "%s" "${tmp}" +} + +# @description Prints the given text to centre of terminal. +# +# @example +# format::text_center "This text is in centre of the terminal." "-" +# +# @arg $1 string Text to be printed. +# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::text_center() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare input="${1}" symbol="${2:- }" filler out no_ansi_out + no_ansi_out=$(format::strip_ansi "$input") + declare -i str_len=${#no_ansi_out} + declare -i filler_len="$(((COLUMNS - str_len) / 2))" + + [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" + for ((i = 0; i < filler_len; i++)); do + filler+="${symbol}" + done + + out="${filler}${input}${filler}" + [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" + printf "%s" "${out}" +} + +# @description Format String to print beautiful report. +# +# @example +# format::report "Initialising mission state" "Success" +# #Output +# Initialising mission state ....................................................................[ Success ] +# +# @arg $1 string Text to be printed on the left. +# @arg $2 string Text to be printed within the square brackets. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout formatted text. +format::report() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare symbol="." to_print y hl hlout out + declare input1="${1} " input2="${2}" + input2="[ $input2 ]" + to_print="$((COLUMNS * 60 / 100))" + y=$(( to_print - ( ${#input1} + ${#input2} ) )) + hl="$(printf '%*s' $y '')" + hlout=${hl// /${symbol}} + out="${input1}${hlout}${input2}" + printf "%s\n" "${out}" +} + +# @description Trim given text to width of the terminal window. +# +# @example +# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." +# #Output +# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. +# +# @arg $1 string Text of first sentence. +# @arg $2 string Text of second sentence (optional). +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout trimmed text. +format::trim_text_to_term() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 + + declare to_print out input1="$1" input2="$2" + if [[ $# = 1 ]]; then + to_print="$((COLUMNS * 93 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } + else + to_print="$((COLUMNS * 40 / 100))" + { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } + to_print="$((COLUMNS * 53 / 100))" + { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } + fi + printf "%s" "$out" +} + +__init diff --git a/src/configng/functions/bash-utility-master/src/interaction.sh b/src/configng/functions/bash-utility-master/src/interaction.sh new file mode 100644 index 000000000..910b60e1c --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/interaction.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# @file Interaction +# @brief Functions to enable interaction with the user. + +# @description Prompt yes or no question to the user. +# +# @example +# interaction::prompt_yes_no "Are you sure to proceed" "yes" +# #Output +# Are you sure to proceed (y/n)? [y] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer \[yes/no\] (optional). +# +# @exitcode 0 If user responds with yes. +# @exitcode 1 If user responds with no. +# @exitcode 2 Function missing arguments. +interaction::prompt_yes_no() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare def_arg response + def_arg="" + response="" + + case "${2}" in + [yY] | [yY][eE][sS]) + def_arg=y + ;; + [nN] | [nN][oO]) + def_arg=n + ;; + esac + + while :; do + printf "%s (y/n)? " "${1}" + [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -z "${response}" ]] && response="${def_arg}" + + case "${response}" in + [yY] | [yY][eE][sS]) + response=y + break + ;; + [nN] | [nN][oO]) + response=n + break + ;; + *) + response="" + ;; + esac + done + + [[ "${response}" = 'y' ]] && return 0 || return 1 +} + +# @description Prompt question to the user. +# +# @example +# interaction::prompt_response "Choose directory to install" "/home/path" +# #Output +# Choose directory to install? [/home/path] +# +# @arg $1 string The question to be prompted to the user. +# @arg $2 string default answer (optional). +# +# @exitcode 0 If user responds with answer. +# @exitcode 2 Function missing arguments. +# +# @stdout User entered answer to the question. +interaction::prompt_response() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare def_arg response + response="" + def_arg="${2}" + + while :; do + printf "%s ? " "${1}" + [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" + + read -r response + [[ -n "${response}" ]] && break + + if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then + response="${def_arg}" + break + fi + done + + [[ "${response}" = "-" ]] && response="" + + printf "%s" "${response}" +} diff --git a/src/configng/functions/bash-utility-master/src/json.sh b/src/configng/functions/bash-utility-master/src/json.sh new file mode 100644 index 000000000..73476618e --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/json.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# @file Json +# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. + +# @description Extract value from json based on key and position. +# Input to the function can be a pipe output, here-string or file. +# @example +# json::get_value "id" "1" < json_file +# json::get_value "id" <<< "${json_var}" +# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" +# +# @arg $1 string id of the field to fetch. +# @arg $2 int position of value to extract.Defaults to 1.(optional) +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string value of extracted key. +json::get_value() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare LC_ALL=C num="${2:-1}" + grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p +} diff --git a/src/configng/functions/bash-utility-master/src/misc.sh b/src/configng/functions/bash-utility-master/src/misc.sh new file mode 100644 index 000000000..fca9a75bf --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/misc.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# @file Miscellaneous +# @brief Set of miscellaneous helper functions. + +# @internal +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +_is_terminal() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Check if internet connection is available. +# +# @example +# misc::check_internet_connection +# +# @noargs +# +# @exitcode 0 If script can connect to internet. +# @exitcode 1 If script cannot access internet. +misc::check_internet_connection() { + declare check_internet + if _is_terminal; then + check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" + else + check_internet="$(curl --compressed -Is google.com -m 10)" + fi + if [[ -z ${check_internet} ]]; then + return 1 + fi +} + +# @description Get list of process ids based on process name. +# +# @example +# misc::get_pid "chrome" +# #Ouput +# 25951 +# 26043 +# 26528 +# 26561 +# +# @arg $1 Name of the process to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout list of process ids. +misc::get_pid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + pgrep "${1}" +} + +# @description Get user id based on username. +# +# @example +# misc::get_uid "labbots" +# #Ouput +# 1000 +# +# @arg $1 username to search. +# +# @exitcode 0 If match successful. +# @exitcode 2 Function missing arguments. +# +# @stdout string uid for the username. +misc::get_uid() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + user_id=$(id "${1}" 2> /dev/null) + declare -i ret=$? + if [[ $ret -ne 0 ]]; then + printf "No user found with username: %s" "${1}\n" + return 1 + fi + + printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' + + unset user_id +} + +# @description Generate random uuid. +# +# @example +# misc::generate_uuid +# #Ouput +# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c +# +# @noargs +# +# @exitcode 0 If match successful. +# +# @stdout random generated uuid. +misc::generate_uuid() { + C="89ab" + + for ((N=0;N<16;++N)); do + B="$((RANDOM%256))" + + case "$N" in + 6) printf '4%x' "$((B%16))" ;; + 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; + + 3|5|7|9) + printf '%02x-' "$B" + ;; + + *) + printf '%02x' "$B" + ;; + esac + done + + printf '\n' +} diff --git a/src/configng/functions/bash-utility-master/src/os.sh b/src/configng/functions/bash-utility-master/src/os.sh new file mode 100644 index 000000000..78e0d36c5 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/os.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +# @file Operating System +# @brief Functions to detect Operating system and version. + +# @description Identify the OS the function is run on. +# +# @noargs +# +# @example +# os::detect_os +# #Output +# linux +# +# @exitcode 0 If OS is successfully detected. +# @exitcode 1 If unable to detect OS. +# +# @stdout Operating system name (linux, mac or windows). +os::detect_os() { + declare uname os + uname=$(command -v uname) + + case $("${uname}" | tr '[:upper:]' '[:lower:]') in + linux*) + os="linux" + ;; + darwin*) + os="mac" + ;; + msys* | cygwin* | mingw* | nt | win*) + # or possible 'bash on windows' + os="windows" + ;; + *) + return 1 + ;; + esac + printf "%s" "${os}" +} + +# @description Identify the distribution flavour of linux. +# +# @noargs +# +# @example +# os::detect_linux_distro +# #Output +# ubuntu +# @exitcode 0 If Linux distro is successfully detected. +# @exitcode 1 If unable to detect OS distro. +# +# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). +os::detect_linux_distro() { + declare distro + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro="${NAME}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro=$(lsb_release -si) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro="${DISTRIB_ID}" + elif [[ -f /etc/debian_version ]]; then + # Older Debian/Ubuntu/etc. + distro="debian" + elif [[ -f /etc/SuSe-release ]]; then + # Older SuSE/etc. + distro="suse" + elif [[ -f /etc/redhat-release ]]; then + # Older Red Hat, CentOS, etc. + distro="redhat" + else + return 1 + fi + printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' +} + +# @description Identify the Linux version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 20.04 +# +# @exitcode 0 If Linux version is successfully detected. +# @exitcode 1 If unable to detect Linux version. +# +# @stdout Linux OS version number (18.04, 20.04, etc.,). +os::detect_linux_version() { + declare distro_version + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro_version="${VERSION_ID}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro_version=$(lsb_release -sr) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro_version="${DISTRIB_RELEASE}" + else + return 1 + fi + printf "%s" "${distro_version}" +} + +# @description Identify the Linux codename. +# +# @noargs +# +# @example +# os::detect_linux_codename +# #Output +# jammy +# +# @exitcode 0 If Linux codename is successfully detected. +# @exitcode 1 If unable to detect Linux codename. +# +# @stdout Linux OS codename number (buster, jammy, etc.,). +os::detect_linux_codename() { + declare distro_codename + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . "/etc/os-release" + distro_codename="${VERSION_CODENAME}" + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + distro_codename=$(lsb_release -cs) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + distro_codename="${DISTRIB_CODENAME}" + else + return 1 + fi + printf "%s" "${distro_codename}" +} + +# @description Identify the MacOS version. +# +# @noargs +# +# @example +# os::detect_linux_version +# #Output +# 10.15.7 +# @exitcode 0 If MacOS version is successfully detected. +# @exitcode 1 If unable to detect MacOS version. +# +# @stdout MacOS version number (10.15.6, etc.,) +os::detect_mac_version() { + if [[ "$(os::detect_os)" = "mac" ]]; then + declare mac_version + mac_version="$(sw_vers -productVersion)" + printf "%s" "${mac_version}" + else + return 1 + fi +} diff --git a/src/configng/functions/bash-utility-master/src/string.sh b/src/configng/functions/bash-utility-master/src/string.sh new file mode 100644 index 000000000..aa522cb55 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/string.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash + +# @file String +# @brief Functions for string operations and manipulations. + +# @description Strip whitespace from the beginning and end of a string. +# +# @example +# echo "$(string::trim " Hello World! ")" +# #Output +# Hello World! +# +# @arg $1 string The string to be trimmed. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout The trimmed string. +string::trim() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + : "${1#"${1%%[![:space:]]*}"}" + : "${_%"${_##*[![:space:]]}"}" + printf '%s\n' "$_" +} + +# @description Split a string to array by a delimiter. +# +# @example +# array=( $(string::split "a,b,c" ",") ) +# printf "%s" "$(string::split "Hello!World" "!")" +# #Output +# Hello +# World +# +# @arg $1 string The input string. +# @arg $2 string The delimiter string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. +string::split() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare -a arr=() + IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" + printf '%s\n' "${arr[@]}" +} + +# @description Strip characters from the beginning of a string. +# +# @example +# echo "$(string::lstrip "Hello World!" "He")" +# #Output +# llo World! +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::lstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1##$2}" +} + +# @description Strip characters from the end of a string. +# +# @example +# echo "$(string::rstrip "Hello World!" "d!")" +# #Output +# Hello Worl +# +# @arg $1 string The input string. +# @arg $2 string The characters you want to strip. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the modified string. +string::rstrip() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + printf '%s\n' "${1%%$2}" +} + +# @description Make a string lowercase. +# +# @example +# echo "$(string::to_lower "HellO")" +# #Output +# hello +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the lowercased string. +string::to_lower() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1,,}" + else + printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' + fi +} + +# @description Make a string all uppercase. +# +# @example +# echo "$(string::to_upper "HellO")" +# #Output +# HELLO +# +# @arg $1 string The input string. +# +# @exitcode 0 If successful. +# @exitcode 2 Function missing arguments. +# +# @stdout Returns the uppercased string. +string::to_upper() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then + printf '%s\n' "${1^^}" + else + printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' + fi +} + +# @description Check whether the search string exists within the input string. +# +# @example +# string::contains "Hello World!" "lo" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::contains() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_contains hello he + [[ "${1}" == *${2}* ]] +} + +# @description Check whether the input string starts with key string. +# +# @example +# string::starts_with "Hello World!" "He" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::starts_with() { + # Usage: string_starts_with hello he + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + [[ "${1}" == ${2}* ]] +} + +# @description Check whether the input string ends with key string. +# +# @example +# string::ends_with "Hello World!" "d!" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::ends_with() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + # Usage: string_ends_wit hello lo + [[ "${1}" == *${2} ]] +} + +# @description Check whether the input string matches the given regex. +# +# @example +# string::regex "HELLO" "^[A-Z]*$" +# +# @arg $1 string The input string. +# @arg $2 string The search key. +# @exitcode 0 If match found. +# @exitcode 1 If no match found. +# @exitcode 2 Function missing arguments. +string::regex() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + if [[ ${1} =~ ${2} ]]; then + return 0 + else + return 1 + fi + +} diff --git a/src/configng/functions/bash-utility-master/src/terminal.sh b/src/configng/functions/bash-utility-master/src/terminal.sh new file mode 100644 index 000000000..d73331d7a --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/terminal.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# @file Terminal +# @brief Set of useful terminal functions. + +# @description Check if script is run in terminal. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +terminal::is_term() { + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +# @description Detect profile rc file for zsh and bash of current script running user. +# +# @noargs +# +# @exitcode 0 If script is run on terminal. +# @exitcode 1 If script is not run on terminal. +# +# @stdout path to the profile file. +terminal::detect_profile() { + declare CURRENT_SHELL="${SHELL##*/}" + case "${CURRENT_SHELL}" in + 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; + 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; + *) if [[ -f "${HOME}/.profile" ]]; then + DETECTED_PROFILE="${HOME}/.profile" + else + printf "No compaitable shell file\n" && exit 1 + fi ;; + esac + printf "%s\n" "${DETECTED_PROFILE}" +} + +# @description Clear the output in terminal on the specified line number. +# This function clears line only on terminal. +# +# @arg $1 Line number to clear. Defaults to 1. (optional) +# +# @exitcode 0 If script is run on terminal. +# +# @stdout clear line ansi code. +terminal::clear_line() { + if terminal::is_term; then + declare line=${1:-1} + printf "\033[%sA\033[2K" "${line}" + fi +} diff --git a/src/configng/functions/bash-utility-master/src/validation.sh b/src/configng/functions/bash-utility-master/src/validation.sh new file mode 100644 index 000000000..37fde1cbc --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/validation.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash + +# @file Validation +# @brief Functions to perform validation on given data. + +# @description Validate whether a given input is a valid email address or not. +# +# @example +# test='test@gmail.com' +# validation::email "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string input email address to validate. +# +# @exitcode 0 If provided input is an email address. +# @exitcode 1 If provided input is not an email address. +# @exitcode 2 Function missing arguments. +validation::email() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare email_re + email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" + [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 +} + +# @description Validate whether a given input is a valid IP V4 address. +# +# @example +# ips=' +# 4.2.2.2 +# a.b.c.d +# 192.168.1.1 +# 0.0.0.0 +# 255.255.255.255 +# 255.255.255.256 +# 192.168.0.1 +# 192.168.0 +# 1234.123.123.123 +# 0.192.168.1 +# ' +# for ip in $ips; do +# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi +# printf "%-20s: %s\n" "$ip" "$stat" +# done +# #Output +# 4.2.2.2 : good +# a.b.c.d : bad +# 192.168.1.1 : good +# 0.0.0.0 : good +# 255.255.255.255 : good +# 255.255.255.256 : bad +# 192.168.0.1 : good +# 192.168.0 : bad +# 1234.123.123.123 : bad +# 0.192.168.1 : good +# +# @arg $1 string input IPv4 address. +# +# @exitcode 0 If provided input is a valid IPv4. +# @exitcode 1 If provided input is not a valid IPv4. +# @exitcode 2 Function missing arguments. +validation::ipv4() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + declare ip="${1}" + declare IFS=. + # shellcheck disable=SC2206 + declare -a a=($ip) + [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 + # Test values of quads + declare quad + for quad in {0..3}; do + [[ "${a[$quad]}" -gt 255 ]] && return 1 + done + return 0 +} + +# @description Validate whether a given input is a valid IP V6 address. +# +# @example +# ips=' +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 +# fe80::1ff:fe23:4567:890a +# fe80::1ff:fe23:4567:890a%eth2 +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar +# fezy::1ff:fe23:4567:890a +# :: +# 2001:db8:: +# ' +# for ip in $ips; do +# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi +# printf "%-50s= %s\n" "$ip" "$stat" +# done +# #Output +# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good +# fe80::1ff:fe23:4567:890a = good +# fe80::1ff:fe23:4567:890a%eth2 = good +# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad +# fezy::1ff:fe23:4567:890a = bad +# :: = good +# 2001:db8:: = good +# +# @arg $1 string input IPv6 address. +# +# @exitcode 0 If provided input is a valid IPv6. +# @exitcode 1 If provided input is not a valid IPv6. +# @exitcode 2 Function missing arguments. +validation::ipv6() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare ip="${1}" + declare 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]))$" + + [[ "${ip}" =~ $re ]] && return 0 || return 1 +} + +# @description Validate if given variable is entirely alphabetic characters. +# +# @example +# test='abcABC' +# validation::alpha "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is only alpha characters. +# @exitcode 1 If input contains any non alpha characters. +# @exitcode 2 Function missing arguments. +validation::alpha() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable contains only alpha-numeric characters. +# +# @example +# test='abc123' +# validation::alpha_num "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is an alpha-numeric. +# @exitcode 1 If input is not an alpha-numeric. +# @exitcode 2 Function missing arguments. +validation::alpha_num() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alnum:]]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. +# +# @example +# test='abc-ABC_cD' +# validation::alpha_dash "${test}" +# echo $? +# #Output +# 0 +# +# @arg $1 string Value of variable to validate. +# +# @exitcode 0 If input is valid. +# @exitcode 1 If input the input is not valid. +# @exitcode 2 Function missing arguments. +validation::alpha_dash() { + [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 + + declare re='^[[:alpha:]_-]+$' + if [[ "${1}" =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. +# +# @arg $1 string Version number to check (eg: 1.0.1) +# $arg $2 string Version number to check (eg: 1.0.1) +# +# @example +# test='abc-ABC_cD' +# validation::version_comparison "12.0.1" "12.0.1" +# echo $? +# #Output +# 0 +# +# @exitcode 0 version number is equal. +# @exitcode 1 $1 version number is greater than $2. +# @exitcode 2 $1 version number is less than $2. +# @exitcode 3 Function is missing required arguments. +# @exitcode 4 Provided input argument is in invalid format. +validation::version_comparison() { + [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 + + declare regex="^[.0-9]*$" + ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 + ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 + + if [[ "$1" == "$2" ]]; then + return 0 + fi + declare IFS=. + declare -a ver1 ver2 + read -r -a ver1 <<<"${1}" + read -r -a ver2 <<<"${2}" + # fill empty fields in ver1 with zeros + for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do + ver1[i]=0 + done + for ((i = 0; i < ${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + # fill empty fields in ver2 with zeros + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 + fi + done + return 0 +} diff --git a/src/configng/functions/bash-utility-master/src/variable.sh b/src/configng/functions/bash-utility-master/src/variable.sh new file mode 100644 index 000000000..67e9fab55 --- /dev/null +++ b/src/configng/functions/bash-utility-master/src/variable.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# @file Variable +# @brief Functions for handling variables. + +# @description Check if given variable is array. +# Pass the variable name instead of value of the variable. +# +# @example +# arr=("a" "b" "c") +# variable::is_array "arr" +# #Output +# 0 +# +# @arg $1 string name of the variable to check. +# +# @exitcode 0 If input is array. +# @exitcode 1 If input is not an array. +variable::is_array() { + if [[ -z "${1}" ]]; then + return 1 + else + declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 + fi + return 1 +} + +# @description Check if given variable is a number. +# +# @example +# variable::is_numeric "1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is number. +# @exitcode 1 If input is not a number. +variable::is_numeric() { + declare re='^[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is an integer. +# +# @example +# variable::is_int "+1234" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is an integer. +# @exitcode 1 If input is not an integer. +variable::is_int() { + declare re='^[+-]?[0-9]+$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a float. +# +# @example +# variable::is_float "+1234.0" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a float. +# @exitcode 1 If input is not a float. +variable::is_float() { + declare re='^[+-]?[0-9]+.?[0-9]*$' + if [[ ${1} =~ $re ]]; then + return 0 + fi + return 1 +} + +# @description Check if given variable is a boolean. +# +# @example +# variable::is_bool "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is a boolean. +# @exitcode 1 If input is not a boolean. +variable::is_bool() { + [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 +} + +# @description Check if given variable is a true. +# +# @example +# variable::is_true "true" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is true. +# @exitcode 1 If input is not true. +variable::is_true() { + [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 +} + +# @description Check if given variable is false. +# +# @example +# variable::is_false "false" +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is false. +# @exitcode 1 If input is not false. +variable::is_false() { + [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 +} + +# @description Check if given variable is empty or null. +# +# @example +# test='' +# variable::is_empty_or_null $test +# #Output +# 0 +# +# @arg $1 mixed Value of variable to check. +# +# @exitcode 0 If input is empty or null. +# @exitcode 1 If input is not empty or null. +variable::is_empty_or_null() { + [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 +} diff --git a/functions/cpu.sh b/src/configng/functions/cpu.sh similarity index 100% rename from functions/cpu.sh rename to src/configng/functions/cpu.sh diff --git a/test/cpu_test.sh b/src/configng/test/cpu_test.sh old mode 100755 new mode 100644 similarity index 100% rename from test/cpu_test.sh rename to src/configng/test/cpu_test.sh From ffb48b4a3565036156f0ab60ed064eca1767a6f3 Mon Sep 17 00:00:00 2001 From: tearran Date: Wed, 12 Jul 2023 18:59:24 -0700 Subject: [PATCH 10/48] moved options --- bin/cpu_test.sh | 114 +----------------------------------------------- 1 file changed, 1 insertion(+), 113 deletions(-) diff --git a/bin/cpu_test.sh b/bin/cpu_test.sh index 9a50d4954..2390654e5 100755 --- a/bin/cpu_test.sh +++ b/bin/cpu_test.sh @@ -58,7 +58,7 @@ check_return(){ fi } -see_cpu(){ + # Get policy declare -i policy=$(cpu::get_policy) printf 'Policy = %d\n' "$policy" @@ -100,115 +100,3 @@ cpu::set_freq $policy "$min_freq" "$max_freq" performance printf "\nAfter\n" cat /etc/default/cpufrequtils -} - -readarray -t functionarray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh") -readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$libpath/configng/cpu.sh" | sed 's/.*:://') -readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$libpat/configng/cpu.sh" | sed 's/::.*//') -readarray -t descriptionarray < <(grep -oP '^# @description.*' "$libpath/configng/cpu.sh" | sed 's/^# @description //') - -#printf '%s\n' "${functionarray[@]}" -#exit 0 -see_help(){ - - echo "" - echo "Usage: ${filename%.*} [ -h | -dev | ]" - echo -e "Options:" - echo -e " -h Print this help." - echo -e " -dev Options:" - for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" - done - echo -e " -cpu Options:" - for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s\n' "${funnamearray[i]}" "${descriptionarray[i]}" - done - - } -# check for -dev -h options -check_opts_test1() -{ - if [[ "$1" == -dev ]] ; then - default="bash" - local found=false - for i in "${!functionarray[@]}"; do - if [ "$2" == "${functionarray[i]}" ]; then - "${functionarray[i]}" - found=true - break - fi - done - if ! $found; then - see_help - exit 0 - fi - elif [[ "$1" == "-cpu" ]] ; then - echo -e " " - echo -e " TODO:" - for i in "${!functionarray[@]}"; do - - printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" - done - - elif [[ "$1" == -h ]] ; then - see_help - else - see_cpu - fi -} - -# check for -h -dev -# if -dev check for number -check_opts_test2(){ - -if [ "$1" == "-dev" ]; then - shift # Shifts the arguments to the left, excluding the first argument ("-dev") - function_name="$1" # Assigns the next argument as the function name - shift # Shifts the arguments again to exclude the function name - - case "$function_name" in - 0) echo "Calling function 0 with arguments: $@" ;; - 1) echo "Calling function 1 with arguments: $@" ;; - 2) echo "Calling function 2 with arguments: $@" ;; - 3) echo "Calling function 3 with arguments: $@" ;; - 4) echo "Calling function 4 with arguments: $@" ;; - *) echo "Invalid function name" ;; - esac -else - echo "No -dev flag found" -fi - -} -# check for -h -dev @ $1 -# if -dev check @ $1 remove and shift $1 to check for x -check_opts() { - if [ "$1" == "-dev" ]; then - shift # Shifts the arguments to the left, excluding the first argument ("-dev") - function_name="$1" # Assigns the next argument as the function name - shift # Shifts the arguments again to exclude the function name - - found=false - - for ((i=0; i<${#functionarray[@]}; i++)); do - if [ "$function_name" == "${functionarray[i]}" ]; then - found=true - ${functionarray[i]} "$@" - break - fi - done - - if [ "$found" == false ]; then - echo "Invalid function name" - fi - - elif [ "$1" == "-h" ]; then - see_help - else - see_cpu - fi -} - -#check_opts_test1 "$@" -check_opts "$@" - -#cpu::set_freq $policy "$min_freq" "$max_freq" performance From 0dd19719f4de254653974305e8bc27f4443b7db2 Mon Sep 17 00:00:00 2001 From: tearran Date: Fri, 14 Jul 2023 21:53:35 -0700 Subject: [PATCH 11/48] removed test --- bin/jampi-config | 212 ----------------------------------------------- 1 file changed, 212 deletions(-) delete mode 100755 bin/jampi-config diff --git a/bin/jampi-config b/bin/jampi-config deleted file mode 100755 index 4dd17d7c6..000000000 --- a/bin/jampi-config +++ /dev/null @@ -1,212 +0,0 @@ -#!/bin/bash - -# -# Copyright (c) 2023 Joseph C Turner -# All rights reserved. -# -# This script. -# demonstrates the compatibility of multiple interfaces for displaying menus or messages. -# It uses an array to set the options for all three menus (bash, whiptail, and dialog). -# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. -# If neither of these programs is available, it falls back to using bash. -# while both are installed falls back to whiptail to display the menu. -# The user can override the default program by passing an argument when running the script: -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# - - - -#DIRECTORY variable to the absolute path of the script's directory -#directory=$(cd "$(dirname "$0")" && pwd) -directory="$(dirname "$(readlink -f "$0")")" -filename=$(basename "${BASH_SOURCE[0]}") -selfpath="$directory"/"$filename" -#libpath="${directory}"/"its-lib" #Include these scripts Library -libpath="$selfpath" - - -clear -# Check for the availability of whiptail and dialog command-line programs -# Set the default program to use for displaying messages -( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" -( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" -( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" - -# if both whiptail and dialog change to prefered -( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" - - -[[ "$1" == -b ]] && default="bash" -[[ "$1" == -w ]] && default="whiptail" -[[ "$1" == -n ]] && default="dialog" - -[[ "$1" == -m ]] && { -export NEWT_COLORS=" -root=blue,black -border=green,black -title=green,black -roottext=red,black -window=red,black -textbox=white,black -button=black,green -compactbutton=white,black -listbox=white,black -actlistbox=black,white -actsellistbox=black,green -checkbox=green,black -actcheckbox=black,green -" -} - -# Check the cpu architecture. for later handeling if nessery -architecture=$(dpkg --print-architecture) -[[ "$architecture" == "armf" ]] && true ; - -#setup menu arrays -readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) -readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) -readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) - -## System@Settings:Advanced Settings (armbian-config) -cmd_advance() -{ - sudo armbian-config -} - -## System@CPU info:Example from Bash Utility (cpu_test.sh) -cmd_cpu_info() -{ - [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; - [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - - -## System@CPU info:An example function (lscpu) -cmd_cpu_ls() -{ - - shell_command="$(lscpu)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - -## System@Bootup Time:An example function (systemd-analyze time) -cmd_boot_time() -{ - - shell_command="$(systemd-analyze time)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - -## System@Login Info :An example function (Login Info) -cmd_lslogins() -{ - - shell_command="$(lslogins)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - - -# Function to display a message using whiptail, dialog or printf depending on what is available on the system -see_message() -{ - # Use if neither whiptail nor dialog are available on the system - if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then - # Use printf to display the message - printf '%s ' "${shell_command[@]}" - - # Use as default if whiptail is available - elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then - # Use whiptail to display the message - whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext - - # Use if dialog is available on the system but not whiptail - elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then - # Use dialog to display the message - dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear - fi -} - -see_menu() -{ - - if [[ "$default" == "bash" ]]; then - PS3="Enter a number: " - select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done - - elif [[ "$default" == "whiptail" ]]; then - OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) - [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" - - elif [[ "$default" == "dialog" ]]; then - OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) - [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" - fi -} - -see_help(){ - - echo "" - echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" - echo -e "Options:" - echo -e " -h Print this help." - echo -e " -b GNU bash " - echo -e " -n NCURSES dialog " - echo -e " -w NEWT whiptail - default colors " - echo -e " -m dark mode whiptail " - echo -e " -dev Options:" - for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" - done - - } - -main() -{ - if [[ "$1" == --dev ]] ; then - default="bash" - local found=false - for i in "${!functionarray[@]}"; do - if [ "$2" == "${functionarray[i]}" ]; then - "${functionarray[i]}" - found=true - break - fi - done - if ! $found; then - see_help - exit 0 - fi - elif [[ "$1" == -h ]] ; then - see_help - else - see_menu - fi -} - -main "$@" From 8bbc6a4dbea93826d133fbb46df9ab6b0e3f1bc4 Mon Sep 17 00:00:00 2001 From: tearran Date: Fri, 14 Jul 2023 22:22:29 -0700 Subject: [PATCH 12/48] renamed: bin/cli_options.sh -> bin/configng.sh deleted: bin/cpu_test.sh new file: bin/jampi-config modified: lib/configng/cpu.sh new file: lib/configng/storage.sh --- bin/{cli_options.sh => configng.sh} | 53 +++++-- bin/cpu_test.sh | 102 ------------- bin/jampi-config | 212 ++++++++++++++++++++++++++++ lib/configng/cpu.sh | 69 +-------- lib/configng/storage.sh | 69 +++++++++ 5 files changed, 320 insertions(+), 185 deletions(-) rename bin/{cli_options.sh => configng.sh} (57%) delete mode 100755 bin/cpu_test.sh create mode 100644 bin/jampi-config create mode 100644 lib/configng/storage.sh diff --git a/bin/cli_options.sh b/bin/configng.sh similarity index 57% rename from bin/cli_options.sh rename to bin/configng.sh index 0062725b6..136a0409d 100644 --- a/bin/cli_options.sh +++ b/bin/configng.sh @@ -12,28 +12,51 @@ directory="$(dirname "$(readlink -f "$0")")" filename=$(basename "${BASH_SOURCE[0]}") libpath="$directory/../lib" -selfpath="$libpath/configng/cpu.sh" +#selfpath="$libpath/configng/cpu.sh" if [[ -d "$directory/../lib" ]]; then libpath="$directory"/../lib -elif [[ -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then - libpath="/usr/lib" +#elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then +# libpath="/usr/lib" else echo "Libraries not found" exit 0 fi +## Source the files relative to the script location +#source "$libpath/bash-utility/string.sh" +#source "$libpath/bash-utility/collection.sh" +#source "$libpath/bash-utility/array.sh" +#source "$libpath/bash-utility/check.sh" +#source "$libpath/configng/cpu.sh" + # Source the files relative to the script location -source "$libpath/bash-utility/string.sh" -source "$libpath/bash-utility/collection.sh" -source "$libpath/bash-utility/array.sh" -source "$libpath/bash-utility/check.sh" -source "$libpath/configng/cpu.sh" +for file in "$libpath"/bash-utility/*; do + source "$file" +done +for file in "$libpath"/configng/*; do + source "$file" +done + +functionarray=() +funnamearray=() +catagoryarray=() +descriptionarray=() + +for file in "$libpath"/configng/*.sh; do + mapfile -t temp_functionarray < <(grep -oP '^\w+::\w+' "$file") + functionarray+=("${temp_functionarray[@]}") + + mapfile -t temp_funnamearray < <(grep -oP '^\w+::\w+' "$file" | sed 's/.*:://') + funnamearray+=("${temp_funnamearray[@]}") + + mapfile -t temp_catagoryarray < <(grep -oP '^\w+::\w+' "$file" | sed 's/::.*//') + catagoryarray+=("${temp_catagoryarray[@]}") + + mapfile -t temp_descriptionarray < <(grep -oP '^# @description.*' "$file" | sed 's/^# @description //') + descriptionarray+=("${temp_descriptionarray[@]}") +done -readarray -t functionarray < <(grep -oP '^\w+::\w+' "$selfpath") -readarray -t funnamearray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/.*:://') -readarray -t catagoryarray < <(grep -oP '^\w+::\w+' "$selfpath" | sed 's/::.*//') -readarray -t descriptionarray < <(grep -oP '^# @description.*' "$selfpath" | sed 's/^# @description //') # test array #printf '%s\n' "${functionarray[@]}" @@ -42,7 +65,7 @@ readarray -t descriptionarray < <(grep -oP '^# @description.*' "$selfpath" | sed see_help(){ echo "" - echo "Usage: ${filename%.*} [ -h | -dev ]" + echo "Usage: ${filename%.*} [ -h | dev ]" echo -e "Options:" echo -e " -h Print this help." echo -e " dev Options:" @@ -52,7 +75,7 @@ see_help(){ } -# TEST 3 +# TEST 3 # check for -h -dev @ $1 # if -dev check @ $1 remove and shift $1 to check for x check_opts() { @@ -81,7 +104,7 @@ check_opts() { echo "Disabled durring current testing" else - see_help + see_help fi } diff --git a/bin/cpu_test.sh b/bin/cpu_test.sh deleted file mode 100755 index 2390654e5..000000000 --- a/bin/cpu_test.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related tests. -# - - -directory="$(dirname "$(readlink -f "$0")")" -filename=$(basename "${BASH_SOURCE[0]}") - -#libpath="$directory/../lib" -#selfpath="$libpath/configng/cpu.sh" - -if [[ -d "$directory/../lib" ]]; then - libpath="$directory"/../lib -elif [[ -d "/usr/lib/bash-utility" && -d "/usr/lib/configng" ]]; then - libpath="/usr/lib" -elif [[ -d "/../functions/bash-utility-master/src" ]] ; then - libpath="$directory"/../functions/bash-utility-master/src -else - echo "Libraries not found" - exit 0 -fi - -# Source the files relative to the script location -source "$libpath/bash-utility/string.sh" -source "$libpath/bash-utility/collection.sh" -source "$libpath/bash-utility/array.sh" -source "$libpath/bash-utility/check.sh" -source "$libpath/configng/cpu.sh" - -# Rest of the script... -# @description Print value from collection. -# -# @example -# collection::each "print_func" -# #Output -# Value in collection -print_func(){ - printf "%s\n" "$1" - return 0 - } - -# @description Check function exit code and exit script if != 0. -# -# @example -# check_return -# #Output -# Nothing -check_return(){ - if [ "$?" -ne 0 ]; then - exit 1 - fi - } - - -# Get policy -declare -i policy=$(cpu::get_policy) -printf 'Policy = %d\n' "$policy" -declare -i min_freq=$(cpu::get_min_freq $policy) -check_return -printf 'Minimum frequency = %d\n' "$min_freq" -declare -i max_freq=$(cpu::get_max_freq $policy) -check_return -printf 'Maximum frequency = %d\n' "$max_freq" -governor=$(cpu::get_governor $policy) -check_return -printf 'Governor = %s\n' "$governor" - -# Return frequencies as array -declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) -check_return -printf "\nAll frequencies\n" - -# Print all values in collection -printf "%s\n" "${freqs[@]}" | collection::each "print_func" -declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) -check_return -printf "\nAll governors\n" - - -# Print all values in collection -printf "%s\n" "${governors[@]}" | collection::each "print_func" - - -# Are we running as sudo? -[[ "$EUID" != 0 ]] && printf "Must call cpu::set_freq as sudo\n" && exit 1 - -# Before -printf "\nBefore\n" -cat /etc/default/cpufrequtils -cpu::set_freq $policy "$min_freq" "$max_freq" performance - -# After -printf "\nAfter\n" -cat /etc/default/cpufrequtils - diff --git a/bin/jampi-config b/bin/jampi-config new file mode 100644 index 000000000..4dd17d7c6 --- /dev/null +++ b/bin/jampi-config @@ -0,0 +1,212 @@ +#!/bin/bash + +# +# Copyright (c) 2023 Joseph C Turner +# All rights reserved. +# +# This script. +# demonstrates the compatibility of multiple interfaces for displaying menus or messages. +# It uses an array to set the options for all three menus (bash, whiptail, and dialog). +# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. +# If neither of these programs is available, it falls back to using bash. +# while both are installed falls back to whiptail to display the menu. +# The user can override the default program by passing an argument when running the script: +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + + + +#DIRECTORY variable to the absolute path of the script's directory +#directory=$(cd "$(dirname "$0")" && pwd) +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") +selfpath="$directory"/"$filename" +#libpath="${directory}"/"its-lib" #Include these scripts Library +libpath="$selfpath" + + +clear +# Check for the availability of whiptail and dialog command-line programs +# Set the default program to use for displaying messages +( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" +( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" +( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" + +# if both whiptail and dialog change to prefered +( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" + + +[[ "$1" == -b ]] && default="bash" +[[ "$1" == -w ]] && default="whiptail" +[[ "$1" == -n ]] && default="dialog" + +[[ "$1" == -m ]] && { +export NEWT_COLORS=" +root=blue,black +border=green,black +title=green,black +roottext=red,black +window=red,black +textbox=white,black +button=black,green +compactbutton=white,black +listbox=white,black +actlistbox=black,white +actsellistbox=black,green +checkbox=green,black +actcheckbox=black,green +" +} + +# Check the cpu architecture. for later handeling if nessery +architecture=$(dpkg --print-architecture) +[[ "$architecture" == "armf" ]] && true ; + +#setup menu arrays +readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) +readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) +readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) + +## System@Settings:Advanced Settings (armbian-config) +cmd_advance() +{ + sudo armbian-config +} + +## System@CPU info:Example from Bash Utility (cpu_test.sh) +cmd_cpu_info() +{ + [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; + [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +## System@CPU info:An example function (lscpu) +cmd_cpu_ls() +{ + + shell_command="$(lscpu)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Bootup Time:An example function (systemd-analyze time) +cmd_boot_time() +{ + + shell_command="$(systemd-analyze time)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + +## System@Login Info :An example function (Login Info) +cmd_lslogins() +{ + + shell_command="$(lslogins)" ; + message_box=$( printf '%s ' "${shell_command[@]}" ) ; + see_message +} + + +# Function to display a message using whiptail, dialog or printf depending on what is available on the system +see_message() +{ + # Use if neither whiptail nor dialog are available on the system + if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then + # Use printf to display the message + printf '%s ' "${shell_command[@]}" + + # Use as default if whiptail is available + elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then + # Use whiptail to display the message + whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext + + # Use if dialog is available on the system but not whiptail + elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then + # Use dialog to display the message + dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear + fi +} + +see_menu() +{ + + if [[ "$default" == "bash" ]]; then + PS3="Enter a number: " + select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done + + elif [[ "$default" == "whiptail" ]]; then + OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + + elif [[ "$default" == "dialog" ]]; then + OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) + [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" + fi +} + +see_help(){ + + echo "" + echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" + echo -e "Options:" + echo -e " -h Print this help." + echo -e " -b GNU bash " + echo -e " -n NCURSES dialog " + echo -e " -w NEWT whiptail - default colors " + echo -e " -m dark mode whiptail " + echo -e " -dev Options:" + for i in "${!functionarray[@]}"; do + printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" + done + + } + +main() +{ + if [[ "$1" == --dev ]] ; then + default="bash" + local found=false + for i in "${!functionarray[@]}"; do + if [ "$2" == "${functionarray[i]}" ]; then + "${functionarray[i]}" + found=true + break + fi + done + if ! $found; then + see_help + exit 0 + fi + elif [[ "$1" == -h ]] ; then + see_help + else + see_menu + fi +} + +main "$@" diff --git a/lib/configng/cpu.sh b/lib/configng/cpu.sh index 772f9f4b1..e495e89f6 100644 --- a/lib/configng/cpu.sh +++ b/lib/configng/cpu.sh @@ -1,4 +1,4 @@ -#!/bin/bash + # # Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com # @@ -198,70 +198,3 @@ cpu::set_freq(){ return 0 } - -# @description SetUp Virtula spi MTD FLash, Remove spi MTD FLash. -# -# @example -# storage::set_spi_vflash s -# echo $? -# #Output -# -# -# @arg $1 int UnSet. -# @arg $1 int SetUp. -# -# @exitcode 0 If successful. -storage::set_spi_vflash(){ - # TODO handeling - [[ "$1" == "setup" ]] && create_virt_spi - [[ "$1" == "remove" ]] && remove_virt_spi - -} - -create_virt_spi() -{ - # Load the nandsim and mtdblock modules to create a virtual MTD device - - sudo modprobe mtdblock - #sudo modprobe nandsim - # Find the newly created MTD device - if [[ ! -e /dev/mtdblock0 ]]; then - sudo modprobe nandsim - irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') - else - echo "$( sudo ls /dev/mtdblock0 )" - fi - - # Create a symlink to the virtual MTD device with the name "spi0.0" - # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name - if [[ ! -e /dev/mtdblock0 ]]; then - ln -s /dev/$virtual_mtd /dev/mtdblock0 - fi - - # Create the mount point if it doesn't exist - mkdir -p /tmp/boot - - # Mount the virtual MTD device to the mount point - mount -t jffs2 /dev/mtdblock0 /tmp/boot - - # write a file to remove - touch /tmp/boot/Mounted_MTD.txt - - echo "$( sudo ls /dev/mtd* )" - -} - -remove_virt_spi() -{ - # Unmount the virtual MTD device from the mount point - umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') - - # Remove the symlink to the virtual MTD device - rm /dev/mtdblock0 - - # Unload the nandsim and mtdblock modules to remove the virtual MTD device - sudo modprobe -r mtdblock - sudo modprobe -r nandsim - - echo "0" -} diff --git a/lib/configng/storage.sh b/lib/configng/storage.sh new file mode 100644 index 000000000..272e13202 --- /dev/null +++ b/lib/configng/storage.sh @@ -0,0 +1,69 @@ + +# @description Remove simulated MTD spi flash. +# +# @example +# storage::set_spi_vflash +# echo $? +# #Output +# /dev/mtd0 +# /dev/mtd0ro +# /dev/mtdblock0 +# +# @exitcode 0 If successful. +storage::set_spi_vflash(){ + + # Load the nandsim and mtdblock modules to create a virtual MTD device + + sudo modprobe mtdblock + #sudo modprobe nandsim + # Find the newly created MTD device + if [[ ! -e /dev/mtdblock0 ]]; then + sudo modprobe nandsim + irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') + else + echo "$( sudo ls /dev/mtdblock0 )" + fi + + # Create a symlink to the virtual MTD device with the name "spi0.0" + # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name + if [[ ! -e /dev/mtdblock0 ]]; then + ln -s /dev/$virtual_mtd /dev/mtdblock0 + fi + + # Create the mount point if it doesn't exist + mkdir -p /tmp/boot + + # Mount the virtual MTD device to the mount point + mount -t jffs2 /dev/mtdblock0 /tmp/boot + + # write a file to remove + touch /tmp/boot/Mounted_MTD.txt + + echo "$( sudo ls /dev/mtd* )" + +} + + +# @description Remove simulated MTD spi flash. +# +# @example +# storage::rem_spi_vflash +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +storage::rem_spi_vflash(){ + + # Unmount the virtual MTD device from the mount point + umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') + + # Remove the symlink to the virtual MTD device + rm /dev/mtdblock0 + + # Unload the nandsim and mtdblock modules to remove the virtual MTD device + sudo modprobe -r mtdblock + sudo modprobe -r nandsim + + echo "0" +} From 0c707baf210f6a4195e4921919646afe3ad60f08 Mon Sep 17 00:00:00 2001 From: tearran Date: Fri, 14 Jul 2023 22:35:07 -0700 Subject: [PATCH 13/48] new file: lib/configng/system.sh iterating sourcing libraies. migrated Run7ZipBenchmark() from armban monitor. added a basic api to call --- lib/configng/system.sh | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/configng/system.sh diff --git a/lib/configng/system.sh b/lib/configng/system.sh new file mode 100644 index 000000000..fa5b03f58 --- /dev/null +++ b/lib/configng/system.sh @@ -0,0 +1,37 @@ + + +# @description Return policy as int based on original armbian-config logic. +# +# @example +# system::see_7ZipBench +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +# +# @stdout tobd. +system::see_7ZipBench() { + echo -e "Preparing benchmark. Be patient please..." + # Do a quick 7-zip benchmark, check whether binary is there. If not install it + MyTool=$(which 7za || which 7zr) + [ -z "${MyTool}" ] && apt-get -f -qq -y install p7zip && MyTool=/usr/bin/7zr + [ -z "${MyTool}" ] && (echo "No 7-zip binary found and could not be installed. Aborting" >&2 ; exit 1) + # Send CLI monitoring to the background to be able to spot throttling and other problems + MonitoringOutput="$(mktemp /tmp/${0##*/}.XXXXXX)" + trap "rm \"${MonitoringOutput}\" ; exit 0" 0 1 2 3 15 + armbianmonitor -m >${MonitoringOutput} & + MonitoringPID=$! + # run 7-zip benchmarks after waiting 10 seconds to spot whether the system was idle before. + # We run the benchmark a single time by default unless otherwise specified on the command line + RunHowManyTimes=${runs:-1} + sleep 10 + for ((i=1;i<=RunHowManyTimes;i++)); do + "${MyTool}" b + done + # report CLI monitoring results as well + kill ${MonitoringPID} + echo -e "\nMonitoring output recorded while running the benchmark:\n" + sed -e '/^\s*$/d' -e '/^Stop/d' <${MonitoringOutput} + echo -e "\n" +} \ No newline at end of file From 0472307dfe008514a9a399b0a886f31af9dbc006 Mon Sep 17 00:00:00 2001 From: tearran Date: Sat, 15 Jul 2023 04:30:26 -0700 Subject: [PATCH 14/48] modified: bin/configng.sh modified: lib/configng/system.sh --- bin/configng.sh | 70 ++++++++++++++++++++++++++---------------- lib/configng/system.sh | 2 +- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/bin/configng.sh b/bin/configng.sh index 136a0409d..6726f29a5 100644 --- a/bin/configng.sh +++ b/bin/configng.sh @@ -1,4 +1,5 @@ #!/bin/bash + # # Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com # @@ -16,6 +17,7 @@ libpath="$directory/../lib" if [[ -d "$directory/../lib" ]]; then libpath="$directory"/../lib +# installed option todo change when lib location determined #elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then # libpath="/usr/lib" else @@ -23,13 +25,6 @@ else exit 0 fi -## Source the files relative to the script location -#source "$libpath/bash-utility/string.sh" -#source "$libpath/bash-utility/collection.sh" -#source "$libpath/bash-utility/array.sh" -#source "$libpath/bash-utility/check.sh" -#source "$libpath/configng/cpu.sh" - # Source the files relative to the script location for file in "$libpath"/bash-utility/*; do source "$file" @@ -57,29 +52,51 @@ for file in "$libpath"/configng/*.sh; do descriptionarray+=("${temp_descriptionarray[@]}") done + see_help_dev(){ + # Extract unique prefixes + declare -A prefixes + for i in "${!functionarray[@]}"; do + prefix="${functionarray[i]%%::*}" + prefixes["$prefix"]=1 + done -# test array -#printf '%s\n' "${functionarray[@]}" -#exit 0 - -see_help(){ + # Construct usage line + usage="" + for prefix in "${!prefixes[@]}"; do + usage+="[ $prefix::options ]" + done - echo "" - echo "Usage: ${filename%.*} [ -h | dev ]" - echo -e "Options:" - echo -e " -h Print this help." - echo -e " dev Options:" + +# echo "$usage" + echo "Usage: ${filename%.*} [ -h | foo ]" + echo "" + echo -e "Options:" + echo -e " -h) Print this help." + echo -e "" + echo -e " foo) Usage: ${filename%.*} foo $usage:: " + echo "" + + # Group options by prefix + declare -A groups for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s\n' "${functionarray[i]}" "${descriptionarray[i]}" - done + prefix="${functionarray[i]%%::*}" + suffix="${functionarray[i]#*::}" + groups["$prefix"]+=$'\t\t'"$suffix\t${descriptionarray[i]}"$'\n' + done + + # Print grouped options + for group in "${!groups[@]}"; do + echo -e " $group::options" + echo -e "${groups[$group]}" + done +} - } -# TEST 3 +# TEST 7 # check for -h -dev @ $1 # if -dev check @ $1 remove and shift $1 to check for x check_opts() { - if [ "$1" == "dev" ]; then + if [ "$1" == "foo" ]; then shift # Shifts the arguments to the left, excluding the first argument ("-dev") function_name="$1" # Assigns the next argument as the function name shift # Shifts the arguments again to exclude the function name @@ -96,18 +113,19 @@ check_opts() { if [ "$found" == false ]; then echo "Invalid function name" + see_help_dev + exit 1 + fi - elif [[ "$1" == "dev" && "$2" == "cpu::set_freq" ]]; then + elif [[ "$1" == "foo" && "$2" == "cpu::set_freq" ]]; then # Disabled till understood. echo "cpu::set_freq policy min_freq max_freq performance" echo "Disabled durring current testing" else - see_help + see_help_dev fi } -#check_opts_test1 "$@" check_opts "$@" - diff --git a/lib/configng/system.sh b/lib/configng/system.sh index fa5b03f58..b5e3344e7 100644 --- a/lib/configng/system.sh +++ b/lib/configng/system.sh @@ -1,6 +1,6 @@ -# @description Return policy as int based on original armbian-config logic. +# @description 7-zip benchmark based on original armbianmonitor logic. # # @example # system::see_7ZipBench From 463654e62853ff3a2040d930f200527d1f96f64e Mon Sep 17 00:00:00 2001 From: tearran Date: Sat, 15 Jul 2023 21:20:08 -0700 Subject: [PATCH 15/48] new file: lib/configng/benchymark.sh new file: lib/configng/board_led.sh modified: lib/configng/cpu.sh deleted: lib/configng/system.sh renamed: lib/configng/storage.sh -> lib/configng/vdrive.sh set funtion groups as files added change systems Board led behavor https://armbian.atlassian.net/browse/AR-449 tested arguments cpu, heartbeat, and none examples none, cpu, or pass arument some handeling. added banchymark group a migrated armbianmonitor function running armbianmonitor -m added system d boot time funtions. changed storage.sh catagory to vdrive.sh group --- lib/configng/benchymark.sh | 77 ++++++++++++++++++++++++++ lib/configng/board_led.sh | 63 +++++++++++++++++++++ lib/configng/cpu.sh | 13 ++--- lib/configng/system.sh | 37 ------------- lib/configng/{storage.sh => vdrive.sh} | 23 ++++++-- 5 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 lib/configng/benchymark.sh create mode 100644 lib/configng/board_led.sh delete mode 100644 lib/configng/system.sh rename lib/configng/{storage.sh => vdrive.sh} (68%) diff --git a/lib/configng/benchymark.sh b/lib/configng/benchymark.sh new file mode 100644 index 000000000..90a1ef118 --- /dev/null +++ b/lib/configng/benchymark.sh @@ -0,0 +1,77 @@ + +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Benchmark related functions. See +# https://systemd.io/ for more info. +# https://www.7-zip.org/ + +# @description system boot-up performance statistics. +# +# @example +# benchymark::see_systemd $1 (blame time chain) +# #Output +# time (quick check of boot time) +# balme (List modual load times) +# chain () +# +# @exitcode 0 If successful. +# +# @stdout tobd. +benchymark::see_monitor(){ + [[ $1 == "" ]] && clear && armbianmonitor -M ; + [[ $1 == $1 ]] && armbianmonitor "$1" ; + exit 0 + } +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Benchmark related functions. See +# https://systemd.io/ for more info. +# https://www.7-zip.org/ + +# @description system boot-up performance statistics. +# +# @example +# benchymark::see_systemd $1 (blame time chain) +# #Output +# time (quick check of boot time) +# balme (List modual load times) +# chain () +# +# @exitcode 0 If successful. +# +# @stdout tobd. +benchymark::see_boot_blame(){ + + [[ $1 == "blame" ]] && sys_blame=$( systemd-analyze blame ) ; + [[ $1 == "time" || $1 == "" ]] && sys_blame=$( systemd-analyze time ) ; + [[ $1 == "chain" ]] && sys_blame=$( systemd-analyze critical-chain ) ; + printf '%s\n' "${sys_blame[@]}" + exit 0 + } + + +# @description 7-zip benchmark based on original armbianmonitor logic. +# +# @example +# benchymark::see_7ZipBench +# echo $? +# #Output +# TBD +# +# @exitcode 0 If successful. +# +# @stdout tobd. +benchymark::see_7ZipBench() { + echo -e "Preparing benchmark. Be patient please..." + [[ $1 == "" ]] && armbianmonitor -z ; + } diff --git a/lib/configng/board_led.sh b/lib/configng/board_led.sh new file mode 100644 index 000000000..9eea83fa0 --- /dev/null +++ b/lib/configng/board_led.sh @@ -0,0 +1,63 @@ + +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# System boards led monitoring. See +# TBD + +# @description set the Sys board led to montor cpu activity. +# +# @example +# boardLED::set_sysled_cpu +# #Output +# Led blinks to set cpu +# +# @exitcode 0 If successful. +# +# @stdout cpu. +boardled::set_sysled_cpu(){ + + echo cpu | sudo tee /sys/class/leds/*/trigger + + } + +# @description set the Sys board led to montor none. +# +# @example +# boardLED::set_sysled_none +# #Output +# Led blinks to set no +# +# @exitcode 0 If successful. +# +# @stdout none. +boardled::set_sysled_none(){ + + echo none | sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger + } + +# @description See a list of board led options. +# +# @example +# boardLED::set_sysled_none +# #Output +# Led blinks to set no +# +# @exitcode 0 If successful. +# +# @stdout tbd. +boardled::see_sysled(){ + + # the avalible options + readarray triggers_led < <( cat /sys/class/leds/*/trigger ) + # see pass not argument the avalible options + [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; + # Set the systme Led blink to $1 valus + [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; + +} + diff --git a/lib/configng/cpu.sh b/lib/configng/cpu.sh index e495e89f6..6847be043 100644 --- a/lib/configng/cpu.sh +++ b/lib/configng/cpu.sh @@ -7,7 +7,6 @@ # warranty of any kind, whether express or implied. # # CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. -# # @description Return policy as int based on original armbian-config logic. # @@ -20,7 +19,7 @@ # @exitcode 0 If successful. # # @stdout Policy as integer. -cpu::get_policy(){ +cpu::see_policy(){ declare -i policy=0 [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 @@ -43,7 +42,7 @@ cpu::get_policy(){ # @exitcode 2 Function missing arguments. # # @stdout Space delimited string of CPU frequencies. -cpu::get_freqs(){ +cpu::see_freqs(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -69,7 +68,7 @@ cpu::get_freqs(){ # @exitcode 2 Function missing arguments. # # @stdout CPU minimum frequency as string. -cpu::get_min_freq(){ +cpu::see_min_freq(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -94,7 +93,7 @@ cpu::get_min_freq(){ # @exitcode 2 Function missing arguments. # # @stdout CPU maximum frequency as string. -cpu::get_max_freq(){ +cpu::see_max_freq(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -114,7 +113,7 @@ cpu::get_max_freq(){ # performance # # @arg $1 int policy. -cpu::get_governor(){ +cpu::see_governor(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value @@ -134,7 +133,7 @@ cpu::get_governor(){ # performance # # @arg $1 int policy. -cpu::get_governors(){ +cpu::see_governors(){ # Check number of arguments [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 # Build file based on policy value diff --git a/lib/configng/system.sh b/lib/configng/system.sh deleted file mode 100644 index b5e3344e7..000000000 --- a/lib/configng/system.sh +++ /dev/null @@ -1,37 +0,0 @@ - - -# @description 7-zip benchmark based on original armbianmonitor logic. -# -# @example -# system::see_7ZipBench -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -# -# @stdout tobd. -system::see_7ZipBench() { - echo -e "Preparing benchmark. Be patient please..." - # Do a quick 7-zip benchmark, check whether binary is there. If not install it - MyTool=$(which 7za || which 7zr) - [ -z "${MyTool}" ] && apt-get -f -qq -y install p7zip && MyTool=/usr/bin/7zr - [ -z "${MyTool}" ] && (echo "No 7-zip binary found and could not be installed. Aborting" >&2 ; exit 1) - # Send CLI monitoring to the background to be able to spot throttling and other problems - MonitoringOutput="$(mktemp /tmp/${0##*/}.XXXXXX)" - trap "rm \"${MonitoringOutput}\" ; exit 0" 0 1 2 3 15 - armbianmonitor -m >${MonitoringOutput} & - MonitoringPID=$! - # run 7-zip benchmarks after waiting 10 seconds to spot whether the system was idle before. - # We run the benchmark a single time by default unless otherwise specified on the command line - RunHowManyTimes=${runs:-1} - sleep 10 - for ((i=1;i<=RunHowManyTimes;i++)); do - "${MyTool}" b - done - # report CLI monitoring results as well - kill ${MonitoringPID} - echo -e "\nMonitoring output recorded while running the benchmark:\n" - sed -e '/^\s*$/d' -e '/^Stop/d' <${MonitoringOutput} - echo -e "\n" -} \ No newline at end of file diff --git a/lib/configng/storage.sh b/lib/configng/vdrive.sh similarity index 68% rename from lib/configng/storage.sh rename to lib/configng/vdrive.sh index 272e13202..771489f86 100644 --- a/lib/configng/storage.sh +++ b/lib/configng/vdrive.sh @@ -1,8 +1,19 @@ -# @description Remove simulated MTD spi flash. +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Externa Drive related functions. See +# http://linux-mtd.infradead.org/doc/general.html for more info. +# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC + +# @description Set up a simulated MTD spi flash for testing. # # @example -# storage::set_spi_vflash +# extra_drive::set_spi_vflash # echo $? # #Output # /dev/mtd0 @@ -10,7 +21,7 @@ # /dev/mtdblock0 # # @exitcode 0 If successful. -storage::set_spi_vflash(){ +extra_drive::set_spi_vflash(){ # Load the nandsim and mtdblock modules to create a virtual MTD device @@ -44,16 +55,16 @@ storage::set_spi_vflash(){ } -# @description Remove simulated MTD spi flash. +# @description Remove tsting simulated MTD spi flash. # # @example -# storage::rem_spi_vflash +# extra_drive::rem_spi_vflash # echo $? # #Output # 0 # # @exitcode 0 If successful. -storage::rem_spi_vflash(){ +extra_drive::rem_spi_vflash(){ # Unmount the virtual MTD device from the mount point umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') From 44f663517b5cc9d9666cbadff180fd8bd3598f84 Mon Sep 17 00:00:00 2001 From: Joseph Turner Date: Sat, 15 Jul 2023 22:01:57 -0700 Subject: [PATCH 16/48] Update README.md --- README.md | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1653df4c9..325303253 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # configng This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility) -embedded in this project. This allows for functional programming in Bash and also modernizes -the monolithic nature of armbian-config. Error handling and validation are also included. -The idea is to provide an API in Bash that can be called from a TUI, GUI or CLI. Please -follow the coding standards which follow Bash Utility functions. - +embedded in this project. This allows for functional programming in Bash. Error handling and validation are also included. +The idea is to provide an API in Bash that can be called from a Command line interface, Text User interface and others. Why Bash? Well, because it's going to be in every distribution. Striped down distributions may not include Python, C/C++, etc. build/runtime environments @@ -12,9 +9,41 @@ may not include Python, C/C++, etc. build/runtime environments * `sudo apt install git` * `cd ~/` * `git clone https://github.com/armbian/configng.git` -* `cd ~/configng/test` -* `sudo ./cpu_test.sh` -If all goes well you should see all the functions in cpu.sh called and output diaplayed. +* `sudo ~/configng/configng.sh` + +#### If all goes well you should see list or avalible commands +``` +Usage: configng [ -h | foo ] + +Options: + -h) Print this help. + + foo) Usage: configng foo [ boardled::options ][ cpu::options ][ extra_drive::options ][ benchymark::options ]:: + + boardled::options + set_sysled_cpu set the Sys board led to montor cpu activity. + set_sysled_none set the Sys board led to montor none. + see_sysled See a list of board led options. + + cpu::options + see_policy Return policy as int based on original armbian-config logic. + see_freqs Return CPU frequencies as string delimited by space. + see_min_freq Return CPU minimum frequency as string. + see_max_freq Return CPU maximum frequency as string. + see_governor Return CPU governor as string. + see_governors Return CPU governors as string delimited by space. + set_freq Set min, max and CPU governor. + + extra_drive::options + set_spi_vflash Set up a simulated MTD spi flash for testing. + rem_spi_vflash Remove tsting simulated MTD spi flash. + + benchymark::options + see_monitor system boot-up performance statistics. + see_boot_blame system boot-up performance statistics. + see_7ZipBench 7-zip benchmark based on original armbianmonitor logic. + +``` ## Coding standards [Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, @@ -40,7 +69,7 @@ printf '%s\n' "${1##$2}" } ``` -Functions should follow filename::func_name style. Then you can tell just from the name which +Functions should follow ~~filename~~::func_name style. Then you can tell just from the name which file the function is located in. Return codes should also follow a similar pattern: * 0 Successful * 1 Not found From 88897a173a06cbae231f3cfa8fce304fd6b4f858 Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 10:25:33 -0700 Subject: [PATCH 17/48] renamed: bin/configng.sh -> bin/config.sh renamed: lib/configng/benchymark.sh -> lib/config/benchymark.sh renamed: lib/configng/board_led.sh -> lib/config/board_led.sh renamed: lib/configng/cpu.sh -> lib/config/cpu.sh renamed: lib/configng/vdrive.sh -> lib/config/extra_drives.sh deleted: src/ --- bin/{configng.sh => config.sh} | 0 lib/{configng => config}/benchymark.sh | 0 lib/{configng => config}/board_led.sh | 63 +- lib/{configng => config}/cpu.sh | 0 .../vdrive.sh => config/extra_drives.sh} | 160 +- src/bash-utility/CODE_OF_CONDUCT.md | 76 - src/bash-utility/CONTRIBUTING.md | 129 - src/bash-utility/LICENSE | 21 - src/bash-utility/README.md | 3026 ----------------- src/bash-utility/bash_utility.sh | 20 - src/bash-utility/bin/bashdoc.awk | 275 -- src/bash-utility/bin/generate_readme.sh | 400 --- src/bash-utility/image/bash-utility.png | Bin 32396 -> 0 bytes src/bash-utility/image/logo.png | Bin 23716 -> 0 bytes src/bash-utility/src/array.sh | 284 -- src/bash-utility/src/check.sh | 34 - src/bash-utility/src/collection.sh | 287 -- src/bash-utility/src/date.sh | 744 ---- src/bash-utility/src/debug.sh | 49 - src/bash-utility/src/file.sh | 222 -- src/bash-utility/src/format.sh | 183 - src/bash-utility/src/interaction.sh | 96 - src/bash-utility/src/json.sh | 25 - src/bash-utility/src/misc.sh | 121 - src/bash-utility/src/os.sh | 135 - src/bash-utility/src/string.sh | 198 -- src/bash-utility/src/terminal.sh | 51 - src/bash-utility/src/validation.sh | 244 -- src/bash-utility/src/variable.sh | 144 - src/configng/README.md | 62 - .../bash-utility-master/CODE_OF_CONDUCT.md | 76 - .../bash-utility-master/CONTRIBUTING.md | 129 - .../functions/bash-utility-master/LICENSE | 21 - .../functions/bash-utility-master/README.md | 3026 ----------------- .../bash-utility-master/bash_utility.sh | 20 - .../bash-utility-master/bin/bashdoc.awk | 275 -- .../bin/generate_readme.sh | 400 --- .../image/bash-utility.png | Bin 32396 -> 0 bytes .../bash-utility-master/image/logo.png | Bin 23716 -> 0 bytes .../bash-utility-master/src/array.sh | 284 -- .../bash-utility-master/src/check.sh | 34 - .../bash-utility-master/src/collection.sh | 287 -- .../functions/bash-utility-master/src/date.sh | 744 ---- .../bash-utility-master/src/debug.sh | 49 - .../functions/bash-utility-master/src/file.sh | 222 -- .../bash-utility-master/src/format.sh | 183 - .../bash-utility-master/src/interaction.sh | 96 - .../functions/bash-utility-master/src/json.sh | 25 - .../functions/bash-utility-master/src/misc.sh | 121 - .../functions/bash-utility-master/src/os.sh | 168 - .../bash-utility-master/src/string.sh | 198 -- .../bash-utility-master/src/terminal.sh | 51 - .../bash-utility-master/src/validation.sh | 244 -- .../bash-utility-master/src/variable.sh | 144 - src/configng/functions/cpu.sh | 199 -- src/configng/test/cpu_test.sh | 75 - 56 files changed, 112 insertions(+), 14008 deletions(-) rename bin/{configng.sh => config.sh} (100%) rename lib/{configng => config}/benchymark.sh (100%) rename lib/{configng => config}/board_led.sh (59%) rename lib/{configng => config}/cpu.sh (100%) rename lib/{configng/vdrive.sh => config/extra_drives.sh} (96%) delete mode 100644 src/bash-utility/CODE_OF_CONDUCT.md delete mode 100644 src/bash-utility/CONTRIBUTING.md delete mode 100644 src/bash-utility/LICENSE delete mode 100644 src/bash-utility/README.md delete mode 100644 src/bash-utility/bash_utility.sh delete mode 100644 src/bash-utility/bin/bashdoc.awk delete mode 100644 src/bash-utility/bin/generate_readme.sh delete mode 100644 src/bash-utility/image/bash-utility.png delete mode 100644 src/bash-utility/image/logo.png delete mode 100644 src/bash-utility/src/array.sh delete mode 100644 src/bash-utility/src/check.sh delete mode 100644 src/bash-utility/src/collection.sh delete mode 100644 src/bash-utility/src/date.sh delete mode 100644 src/bash-utility/src/debug.sh delete mode 100644 src/bash-utility/src/file.sh delete mode 100644 src/bash-utility/src/format.sh delete mode 100644 src/bash-utility/src/interaction.sh delete mode 100644 src/bash-utility/src/json.sh delete mode 100644 src/bash-utility/src/misc.sh delete mode 100644 src/bash-utility/src/os.sh delete mode 100644 src/bash-utility/src/string.sh delete mode 100644 src/bash-utility/src/terminal.sh delete mode 100644 src/bash-utility/src/validation.sh delete mode 100644 src/bash-utility/src/variable.sh delete mode 100644 src/configng/README.md delete mode 100644 src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md delete mode 100644 src/configng/functions/bash-utility-master/CONTRIBUTING.md delete mode 100644 src/configng/functions/bash-utility-master/LICENSE delete mode 100644 src/configng/functions/bash-utility-master/README.md delete mode 100644 src/configng/functions/bash-utility-master/bash_utility.sh delete mode 100644 src/configng/functions/bash-utility-master/bin/bashdoc.awk delete mode 100644 src/configng/functions/bash-utility-master/bin/generate_readme.sh delete mode 100644 src/configng/functions/bash-utility-master/image/bash-utility.png delete mode 100644 src/configng/functions/bash-utility-master/image/logo.png delete mode 100644 src/configng/functions/bash-utility-master/src/array.sh delete mode 100644 src/configng/functions/bash-utility-master/src/check.sh delete mode 100644 src/configng/functions/bash-utility-master/src/collection.sh delete mode 100644 src/configng/functions/bash-utility-master/src/date.sh delete mode 100644 src/configng/functions/bash-utility-master/src/debug.sh delete mode 100644 src/configng/functions/bash-utility-master/src/file.sh delete mode 100644 src/configng/functions/bash-utility-master/src/format.sh delete mode 100644 src/configng/functions/bash-utility-master/src/interaction.sh delete mode 100644 src/configng/functions/bash-utility-master/src/json.sh delete mode 100644 src/configng/functions/bash-utility-master/src/misc.sh delete mode 100644 src/configng/functions/bash-utility-master/src/os.sh delete mode 100644 src/configng/functions/bash-utility-master/src/string.sh delete mode 100644 src/configng/functions/bash-utility-master/src/terminal.sh delete mode 100644 src/configng/functions/bash-utility-master/src/validation.sh delete mode 100644 src/configng/functions/bash-utility-master/src/variable.sh delete mode 100644 src/configng/functions/cpu.sh delete mode 100644 src/configng/test/cpu_test.sh diff --git a/bin/configng.sh b/bin/config.sh similarity index 100% rename from bin/configng.sh rename to bin/config.sh diff --git a/lib/configng/benchymark.sh b/lib/config/benchymark.sh similarity index 100% rename from lib/configng/benchymark.sh rename to lib/config/benchymark.sh diff --git a/lib/configng/board_led.sh b/lib/config/board_led.sh similarity index 59% rename from lib/configng/board_led.sh rename to lib/config/board_led.sh index 9eea83fa0..473f5d890 100644 --- a/lib/configng/board_led.sh +++ b/lib/config/board_led.sh @@ -9,55 +9,56 @@ # System boards led monitoring. See # TBD -# @description set the Sys board led to montor cpu activity. +# @description See a list of board led options. # # @example -# boardLED::set_sysled_cpu +# boardLED::set_sysled # #Output -# Led blinks to set cpu +# Led blinks to set $1 # # @exitcode 0 If successful. # -# @stdout cpu. -boardled::set_sysled_cpu(){ - - echo cpu | sudo tee /sys/class/leds/*/trigger +# @stdout tbd. +boardled::see_sysled(){ + + # the avalible options + readarray triggers_led < <( cat /sys/class/leds/*/trigger ) + # see pass not argument the avalible options + [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; + # Set the systme Led blink to $1 valus + [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; - } +} -# @description set the Sys board led to montor none. -# -# @example -# boardLED::set_sysled_none -# #Output -# Led blinks to set no +# @description Set board led options to none (off). # # @exitcode 0 If successful. # -# @stdout none. -boardled::set_sysled_none(){ +# @stdout tbd. +boardled::see_sysled_none(){ - echo none | sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger - } + echo "none"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; -# @description See a list of board led options. -# -# @example -# boardLED::set_sysled_none -# #Output -# Led blinks to set no +} + +# @description Set board led options to monitor CPU. # # @exitcode 0 If successful. # # @stdout tbd. -boardled::see_sysled(){ +boardled::see_sysled_cpu(){ - # the avalible options - readarray triggers_led < <( cat /sys/class/leds/*/trigger ) - # see pass not argument the avalible options - [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; - # Set the systme Led blink to $1 valus - [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; + echo "cpu"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; } +# @description Set board led options to heartbeat pulse. +# +# @exitcode 0 If successful. +# +# @stdout tbd. +boardled::see_sysled_beat(){ + + echo "heartbeat"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; + +} diff --git a/lib/configng/cpu.sh b/lib/config/cpu.sh similarity index 100% rename from lib/configng/cpu.sh rename to lib/config/cpu.sh diff --git a/lib/configng/vdrive.sh b/lib/config/extra_drives.sh similarity index 96% rename from lib/configng/vdrive.sh rename to lib/config/extra_drives.sh index 771489f86..6f5797cd3 100644 --- a/lib/configng/vdrive.sh +++ b/lib/config/extra_drives.sh @@ -1,80 +1,80 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Externa Drive related functions. See -# http://linux-mtd.infradead.org/doc/general.html for more info. -# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC - -# @description Set up a simulated MTD spi flash for testing. -# -# @example -# extra_drive::set_spi_vflash -# echo $? -# #Output -# /dev/mtd0 -# /dev/mtd0ro -# /dev/mtdblock0 -# -# @exitcode 0 If successful. -extra_drive::set_spi_vflash(){ - - # Load the nandsim and mtdblock modules to create a virtual MTD device - - sudo modprobe mtdblock - #sudo modprobe nandsim - # Find the newly created MTD device - if [[ ! -e /dev/mtdblock0 ]]; then - sudo modprobe nandsim - irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') - else - echo "$( sudo ls /dev/mtdblock0 )" - fi - - # Create a symlink to the virtual MTD device with the name "spi0.0" - # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name - if [[ ! -e /dev/mtdblock0 ]]; then - ln -s /dev/$virtual_mtd /dev/mtdblock0 - fi - - # Create the mount point if it doesn't exist - mkdir -p /tmp/boot - - # Mount the virtual MTD device to the mount point - mount -t jffs2 /dev/mtdblock0 /tmp/boot - - # write a file to remove - touch /tmp/boot/Mounted_MTD.txt - - echo "$( sudo ls /dev/mtd* )" - -} - - -# @description Remove tsting simulated MTD spi flash. -# -# @example -# extra_drive::rem_spi_vflash -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -extra_drive::rem_spi_vflash(){ - - # Unmount the virtual MTD device from the mount point - umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') - - # Remove the symlink to the virtual MTD device - rm /dev/mtdblock0 - - # Unload the nandsim and mtdblock modules to remove the virtual MTD device - sudo modprobe -r mtdblock - sudo modprobe -r nandsim - - echo "0" -} + +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Externa Drive related functions. See +# http://linux-mtd.infradead.org/doc/general.html for more info. +# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC + +# @description Set up a simulated MTD spi flash for testing. +# +# @example +# extra_drive::set_spi_vflash +# echo $? +# #Output +# /dev/mtd0 +# /dev/mtd0ro +# /dev/mtdblock0 +# +# @exitcode 0 If successful. +extra_drive::set_spi_vflash(){ + + # Load the nandsim and mtdblock modules to create a virtual MTD device + + sudo modprobe mtdblock + #sudo modprobe nandsim + # Find the newly created MTD device + if [[ ! -e /dev/mtdblock0 ]]; then + sudo modprobe nandsim + irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') + else + echo "$( sudo ls /dev/mtdblock0 )" + fi + + # Create a symlink to the virtual MTD device with the name "spi0.0" + # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name + if [[ ! -e /dev/mtdblock0 ]]; then + ln -s /dev/$virtual_mtd /dev/mtdblock0 + fi + + # Create the mount point if it doesn't exist + mkdir -p /tmp/boot + + # Mount the virtual MTD device to the mount point + mount -t jffs2 /dev/mtdblock0 /tmp/boot + + # write a file to remove + touch /tmp/boot/Mounted_MTD.txt + + echo "$( sudo ls /dev/mtd* )" + +} + + +# @description Remove tsting simulated MTD spi flash. +# +# @example +# extra_drive::rem_spi_vflash +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +extra_drive::rem_spi_vflash(){ + + # Unmount the virtual MTD device from the mount point + umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') + + # Remove the symlink to the virtual MTD device + rm /dev/mtdblock0 + + # Unload the nandsim and mtdblock modules to remove the virtual MTD device + sudo modprobe -r mtdblock + sudo modprobe -r nandsim + + echo "0" +} diff --git a/src/bash-utility/CODE_OF_CONDUCT.md b/src/bash-utility/CODE_OF_CONDUCT.md deleted file mode 100644 index 461c926b9..000000000 --- a/src/bash-utility/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at - -For answers to common questions about this code of conduct, see - - -[homepage]: https://www.contributor-covenant.org diff --git a/src/bash-utility/CONTRIBUTING.md b/src/bash-utility/CONTRIBUTING.md deleted file mode 100644 index aa401df88..000000000 --- a/src/bash-utility/CONTRIBUTING.md +++ /dev/null @@ -1,129 +0,0 @@ -# Contributing to Bash-Utility - -:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: -The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. - -## Table of Contents -- [Code Contributions](#code-contributions) -- [Code Guidelines](#code-guidelines) - - [Styleguide](#styleguide) - - [Bashdoc guideline](#bashdoc-guideline) -- [Documentation](#documentation) -- [Commit Guidelines](#commit-guidelines) -- [Pull Request Guidelines](#pull-request-guidelines) -- [Contact](#contact) - -## Code Contributions - -Great, the more, the merrier. - -Sane code contributions are always welcome, whether to the code or documentation. - -Before making a pull request, make sure to follow below guidelines: - -### Code Guidelines - -#### Styleguide - -- Variable names must be meaningful and self-documenting. -- Long variable names must be structured by underscores to improve legibility. -- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE) -- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data) -- Variable names can be alphanumeric with underscores. No special characters in variable names. -- Variables name must not start with number. -- Variables within function must be declared. So the scope of variable is restricted to the function. -- Avoid accessing global variables within functions. -- Function names must be all lower case with underscores to seperate words (snake_case). -- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`) -- Try using bash builtins and string substitution as much as possible. -- Use printf everywhere instead of echo. -- Before adding a new logic, be sure to check the existing code. -- Make sure to add the function in appropriate section based on its operation. -- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command: - - ```shell - shfmt upload.sh - ``` - - The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags. - You can also install shfmt for various editors, refer their repo for information. - Note: This is strictly necessary to maintain consistency, do not skip. - -- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason. - -#### Bashdoc guideline - -The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is -properly generated by the generator. - -Follow the below bashdoc template to add function introductory comment. - -```bash -# @description Multiline description goes here and -# there -# -# @example -# sample::function a b c -# echo 123 -# -# @arg $1 string Some arg. -# @arg $2 any Rest of arguments. -# -# @noargs -# -# @exitcode 0 If successfull. -# @exitcode >0 On failure -# @exitcode 5 On some error. -# -# @stdout Path to something. -# -# @see sample::other_function(() -sample::function() { -} -``` - -- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used. -- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment. -- Make sure to document the exitcode emitted by the function. -- If the function is similar to other function add a reference to function using @see tag. - -### Documentation - -- Refrain from making unnecessary newlines or whitespace. -- Use pure markdown as much as possible, html is accepted but shouldn't be a priority. -- The markdown must pass RemarkLint checks. -- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc. - - ```bash - ./bin/generate_readme.sh -f README.md -s src/ - ``` - -### Commit Guidelines - -It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted. - -It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message. - -The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation. - -Before committing check for unnecessary whitespace with `git diff --check`. - -### Pull Request Guidelines - -The following guidelines will increase the likelihood that your pull request will get accepted: - -- Follow the commit and code guidelines. -- Keep the patches on topic and focused. -- Try to avoid unnecessary formatting and clean-up where reasonable. - -A pull request should contain the following: - -- At least one commit (all of which should follow the Commit Guidelines). -- Title that summarises the issue/feature. -- Description that briefly summarises the changes. - -After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread. - -## Contact - -For further inquiries, you can contact the developer by opening an issue on the repository. diff --git a/src/bash-utility/LICENSE b/src/bash-utility/LICENSE deleted file mode 100644 index 99dd0836b..000000000 --- a/src/bash-utility/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 labbots - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/bash-utility/README.md b/src/bash-utility/README.md deleted file mode 100644 index 9bc1f9484..000000000 --- a/src/bash-utility/README.md +++ /dev/null @@ -1,3026 +0,0 @@ -

Bash Utility

- -

-Stars -License - -

-

-Gh-pages Status -Website -

-

-Total number of Library functions -

-

- -

-Bash library which provides utility functions and helpers for functional programming in Bash. - -Detailed documentation is available at - - - -## Table of Contents - -- [Installation](#installation) - - [Method 1 - Git Submodules](#method-1---git-submodules) - - [Method 2 - Git Clone](#method-2---git-clone) - - [Method 3 - Direct Download](#method-3---direct-download) -- [Usage](#usage) -- [Array](#array) - - [array::contains()](#arraycontains) - - [array::dedupe()](#arraydedupe) - - [array::is_empty()](#arrayis_empty) - - [array::join()](#arrayjoin) - - [array::reverse()](#arrayreverse) - - [array::random_element()](#arrayrandom_element) - - [array::sort()](#arraysort) - - [array::rsort()](#arrayrsort) - - [array::bsort()](#arraybsort) - - [array::merge()](#arraymerge) -- [Check](#check) - - [check::command_exists()](#checkcommand_exists) - - [check::is_sudo()](#checkis_sudo) -- [Collection](#collection) - - [collection::each()](#collectioneach) - - [collection::every()](#collectionevery) - - [collection::filter()](#collectionfilter) - - [collection::find()](#collectionfind) - - [collection::invoke()](#collectioninvoke) - - [collection::map()](#collectionmap) - - [collection::reject()](#collectionreject) - - [collection::some()](#collectionsome) -- [Date](#date) - - [date::now()](#datenow) - - [date::epoc()](#dateepoc) - - [date::add_days_from()](#dateadd_days_from) - - [date::add_months_from()](#dateadd_months_from) - - [date::add_years_from()](#dateadd_years_from) - - [date::add_weeks_from()](#dateadd_weeks_from) - - [date::add_hours_from()](#dateadd_hours_from) - - [date::add_minutes_from()](#dateadd_minutes_from) - - [date::add_seconds_from()](#dateadd_seconds_from) - - [date::add_days()](#dateadd_days) - - [date::add_months()](#dateadd_months) - - [date::add_years()](#dateadd_years) - - [date::add_weeks()](#dateadd_weeks) - - [date::add_hours()](#dateadd_hours) - - [date::add_minutes()](#dateadd_minutes) - - [date::add_seconds()](#dateadd_seconds) - - [date::sub_days_from()](#datesub_days_from) - - [date::sub_months_from()](#datesub_months_from) - - [date::sub_years_from()](#datesub_years_from) - - [date::sub_weeks_from()](#datesub_weeks_from) - - [date::sub_hours_from()](#datesub_hours_from) - - [date::sub_minutes_from()](#datesub_minutes_from) - - [date::sub_seconds_from()](#datesub_seconds_from) - - [date::sub_days()](#datesub_days) - - [date::sub_months()](#datesub_months) - - [date::sub_years()](#datesub_years) - - [date::sub_weeks()](#datesub_weeks) - - [date::sub_hours()](#datesub_hours) - - [date::sub_minutes()](#datesub_minutes) - - [date::sub_seconds()](#datesub_seconds) - - [date::format()](#dateformat) -- [Debug](#debug) - - [debug::print_array()](#debugprint_array) - - [debug::print_ansi()](#debugprint_ansi) -- [File](#file) - - [file::make_temp_file()](#filemake_temp_file) - - [file::make_temp_dir()](#filemake_temp_dir) - - [file::name()](#filename) - - [file::basename()](#filebasename) - - [file::extension()](#fileextension) - - [file::dirname()](#filedirname) - - [file::full_path()](#filefull_path) - - [file::mime_type()](#filemime_type) - - [file::contains_text()](#filecontains_text) -- [Format](#format) - - [format::human_readable_seconds()](#formathuman_readable_seconds) - - [format::bytes_to_human()](#formatbytes_to_human) - - [format::strip_ansi()](#formatstrip_ansi) - - [format::text_center()](#formattext_center) - - [format::report()](#formatreport) - - [format::trim_text_to_term()](#formattrim_text_to_term) -- [Interaction](#interaction) - - [interaction::prompt_yes_no()](#interactionprompt_yes_no) - - [interaction::prompt_response()](#interactionprompt_response) -- [Json](#json) - - [json::get_value()](#jsonget_value) -- [Miscellaneous](#miscellaneous) - - [misc::check_internet_connection()](#misccheck_internet_connection) - - [misc::get_pid()](#miscget_pid) - - [misc::get_uid()](#miscget_uid) - - [misc::generate_uuid()](#miscgenerate_uuid) -- [Operating System](#operating-system) - - [os::detect_os()](#osdetect_os) - - [os::detect_linux_distro()](#osdetect_linux_distro) - - [os::detect_linux_version()](#osdetect_linux_version) - - [os::detect_mac_version()](#osdetect_mac_version) -- [String](#string) - - [string::trim()](#stringtrim) - - [string::split()](#stringsplit) - - [string::lstrip()](#stringlstrip) - - [string::rstrip()](#stringrstrip) - - [string::to_lower()](#stringto_lower) - - [string::to_upper()](#stringto_upper) - - [string::contains()](#stringcontains) - - [string::starts_with()](#stringstarts_with) - - [string::ends_with()](#stringends_with) - - [string::regex()](#stringregex) -- [Terminal](#terminal) - - [terminal::is_term()](#terminalis_term) - - [terminal::detect_profile()](#terminaldetect_profile) - - [terminal::clear_line()](#terminalclear_line) -- [Validation](#validation) - - [validation::email()](#validationemail) - - [validation::ipv4()](#validationipv4) - - [validation::ipv6()](#validationipv6) - - [validation::alpha()](#validationalpha) - - [validation::alpha_num()](#validationalpha_num) - - [validation::alpha_dash()](#validationalpha_dash) - - [validation::version_comparison()](#validationversion_comparison) -- [Variable](#variable) - - [variable::is_array()](#variableis_array) - - [variable::is_numeric()](#variableis_numeric) - - [variable::is_int()](#variableis_int) - - [variable::is_float()](#variableis_float) - - [variable::is_bool()](#variableis_bool) - - [variable::is_true()](#variableis_true) - - [variable::is_false()](#variableis_false) - - [variable::is_empty_or_null()](#variableis_empty_or_null) -- [Inspired By](#inspired-by) -- [License](#license) - - -## Installation -The script can be installed and sourced using following methods. - -### Method 1 - Git Submodules -If the library is used inside a git project then git submodules can be used to install the library to the project. -Following command will initialize git submodule and download the library to `./vendor/bash-utility` folder. - -```shell -git submodule init -git submodule add -b master https://github.com/labbots/bash-utility vendor/bash-utility -``` - -To Update submodules to latest code execute the following command. - -```shell -git submodule update --rebase --remote -``` -Once the submodule is added or updated, make sure to commit changes to your repository. - -```shell -git add . -git commit -m 'Added/updated bash-utility library.' -``` -**Note:** When cloning your repository, use `--recurse-submodules` flag to `git clone` command to install the git sub modules. - -### Method 2 - Git Clone -If you don't want to use git submodules, you can use `git clone` to download library and then move the files to desired location manually. - -The below command will clone the repository to `vendor/bash-utility` folder in current working directory. - -```shell -git clone https://github.com/labbots/bash-utility.git ./vendor/bash-utility -``` -### Method 3 - Direct Download -If you do not have git installed, you can download the archive of the latest version of the library. Extract the zip file to appropriate folder by following the below command. - -```shell -wget https://github.com/labbots/bash-utility/archive/master.zip -unzip -q master.zip -d tmp -mkdir -p vendor/bash-utility -mv tmp/bash-utility-master vendor/bash-utility -rm tmp -``` - -## Usage -Bash utility functions can be used by simply sourcing the library script file to your own script. -To access all the functions within the bash-utility library, you could import the main bash file as follows. - -```shell -source "vendor/bash-utility/bash-utility.sh" -``` - -You can also only use the necessary library functions by only importing the required function files. - -```shell -source "vendor/bash-utility/src/array.sh" -``` - - - -## Array - -Functions for array operations and manipulations. - -### array::contains() - -Check if item exists in the given array. - -#### Arguments - -- **$1** (mixed): Item to search (needle). -- **$2** (array): array to be searched (haystack). - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found in the array. -- **2**: Function missing arguments. - -#### Example - -```bash -array=("a" "b" "c") -array::contains "c" ${array[@]} -#Output -0 -``` - -### array::dedupe() - -Remove duplicate items from the array. - -#### Arguments - -- **$1** (array): Array to be deduped. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Deduplicated array. - -#### Example - -```bash -array=("a" "b" "a" "c") -printf "%s" "$(array::dedupe ${array[@]})" -#Output -a -b -c -``` - -### array::is_empty() - -Check if a given array is empty. - -#### Arguments - -- **$1** (array): Array to be checked. - -#### Exit codes - -- **0**: If the given array is empty. -- **2**: If the given array is not empty. - -#### Example - -```bash -array=("a" "b" "c" "d") -array::is_empty "${array[@]}" -``` - -### array::join() - -Join array elements with a string. - -#### Arguments - -- **$1** (string): String to join the array elements (glue). -- **$2** (array): array to be joined with glue string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- String containing a string representation of all the array elements in the same order,with the glue string between each element. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s" "$(array::join "," "${array[@]}")" -#Output -a,b,c,d -printf "%s" "$(array::join "" "${array[@]}")" -#Output -abcd -``` - -### array::reverse() - -Return an array with elements in reverse order. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The reversed array. - -#### Example - -```bash -array=(1 2 3 4 5) -printf "%s" "$(array::reverse "${array[@]}")" -#Output -5 4 3 2 1 -``` - -### array::random_element() - -Returns a random item from the array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Random item out of the array. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s\n" "$(array::random_element "${array[@]}")" -#Output -c -``` - -### array::sort() - -Sort an array from lowest to highest. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -1 -2 -4 5 -a -a c -d -``` - -### array::rsort() - -Sort an array in reverse order (highest to lowest). - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- reverse sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -d -a c -a -4 5 -2 -1 -``` - -### array::bsort() - -Bubble sort an integer array from lowest to highest. -This sort does not work on string array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- bubble sorted array. - -#### Example - -```bash -iarr=(4 5 1 3) -array::bsort "${iarr[@]}" -#Output -1 -3 -4 -5 -``` - -### array::merge() - -Merge two arrays. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of first array. -- **$2** (string): variable name of second array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Merged array. - -#### Example - -```bash -a=("a" "c") -b=("d" "c") -array::merge "a[@]" "b[@]" -#Output -a -c -d -c -``` - -## Check - -Helper functions. - -### check::command_exists() - -Check if the command exists in the system. - -#### Arguments - -- **$1** (string): Command name to be searched. - -#### Exit codes - -- **0**: If the command exists. -- **1**: If the command does not exist. -- **2**: Function missing arguments. - -#### Example - -```bash -check::command_exists "tput" -``` - -### check::is_sudo() - -Check if the script is executed with sudo privilege. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If the script is executed with root privilege. -- **1**: If the script is not executed with root privilege - -#### Example - -```bash -check::is_sudo -``` - -## Collection - -(Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -### collection::each() - -Iterates over elements of collection and invokes iteratee for each element. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output of iteratee function. - -#### Example - -```bash -test_func(){ - printf "print value: %s\n" "$1" - return 0 - } -arr1=("a b" "c d" "a" "d") -printf "%s\n" "${arr1[@]}" | collection::each "test_func" -collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -#output - print value: a b - print value: c d - print value: a - print value: d -``` - -#### Example - -```bash -# If other function from this library is already used to process the array. -# Then following method could be used to pass the array to the function. -out=("$(array::dedupe "${arr1[@]}")") -collection::each "test_func" <<< "${out[@]}" -``` - -### collection::every() - -Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If iteratee function fails. -- **2**: Function missing arguments. - -#### Example - -```bash -arri=("1" "2" "3" "4") -printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -``` - -### collection::filter() - -Iterates over elements of array, returning all elements where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -#output -1 -2 -3 -``` - -### collection::find() - -Iterates over elements of collection, returning the first element where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Output on stdout - -- first array value matching the iteratee function. - -#### Example - -```bash -arr=("1" "2" "3" "a") -check_a(){ - [[ "$1" = "a" ]] -} -printf "%s\n" "${arr[@]}" | collection::find "check_a" -#Output -a -``` - -### collection::invoke() - -Invokes the iteratee with each element passed as argument to the iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output from the iteratee function. - -#### Example - -```bash -opt=("-a" "-l") -printf "%s\n" "${opt[@]}" | collection::invoke "ls" -``` - -### collection::map() - -Creates an array of values by running each element in array through iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output result of iteratee on value. - -#### Example - -```bash -arri=("1" "2" "3") -add_one(){ - i=${1} - i=$(( i + 1 )) - printf "%s\n" "$i" -} -printf "%s\n" "${arri[@]}" | collection::map "add_one" -``` - -### collection::reject() - -The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values not matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -#Ouput -a -``` - -#### See also - -- [collection::filter](#collectionfilter) - -### collection::some() - -Checks if iteratee returns true for any element of the array. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If match successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -arr=("a" "b" "3" "a") -printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -``` - -## Date - -Functions for manipulating dates. - -### date::now() - -Get current time in unix timestamp. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- current timestamp. - -#### Example - -```bash -echo "$(date::now)" -#Output -1591554426 -``` - -### date::epoc() - -convert datetime string to unix timestamp. - -#### Arguments - -- **$1** (string): date time in any format. - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp for specified datetime. - -#### Example - -```bash -echo "$(date::epoc "2020-07-07 18:38")" -#Output -1594143480 -``` - -### date::add_days_from() - -Add number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days_from "1594143480")" -#Output -1594229880 -``` - -### date::add_months_from() - -Add number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months_from "1594143480")" -#Output -1596821880 -``` - -### date::add_years_from() - -Add number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years_from "1594143480")" -#Output -1625679480 -``` - -### date::add_weeks_from() - -Add number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks_from "1594143480")" -#Output -1594748280 -``` - -### date::add_hours_from() - -Add number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours_from "1594143480")" -#Output -1594147080 -``` - -### date::add_minutes_from() - -Add number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes_from "1594143480")" -#Output -1594143540 -``` - -### date::add_seconds_from() - -Add number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds_from "1594143480")" -#Output -1594143481 -``` - -### date::add_days() - -Add number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days "1")" -#Output -1591640826 -``` - -### date::add_months() - -Add number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months "1")" -#Output -1594146426 -``` - -### date::add_years() - -Add number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years "1")" -#Output -1623090426 -``` - -### date::add_weeks() - -Add number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks "1")" -#Output -1592159226 -``` - -### date::add_hours() - -Add number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours "1")" -#Output -1591558026 -``` - -### date::add_minutes() - -Add number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes "1")" -#Output -1591554486 -``` - -### date::add_seconds() - -Add number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds "1")" -#Output -1591554427 -``` - -### date::sub_days_from() - -Subtract number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days_from "1594143480")" -#Output -1594057080 -``` - -### date::sub_months_from() - -Subtract number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months_from "1594143480")" -#Output -1591551480 -``` - -### date::sub_years_from() - -Subtract number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years_from "1594143480")" -#Output -1562521080 -``` - -### date::sub_weeks_from() - -Subtract number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks_from "1594143480")" -#Output -1593538680 -``` - -### date::sub_hours_from() - -Subtract number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours_from "1594143480")" -#Output -1594139880 -``` - -### date::sub_minutes_from() - -Subtract number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes_from "1594143480")" -#Output -1594143420 -``` - -### date::sub_seconds_from() - -Subtract number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds_from "1594143480")" -#Output -1594143479 -``` - -### date::sub_days() - -Subtract number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days "1")" -#Output -1588876026 -``` - -### date::sub_months() - -Subtract number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months "1")" -#Output -1559932026 -``` - -### date::sub_years() - -Subtract number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years "1")" -#Output -1591468026 -``` - -### date::sub_weeks() - -Subtract number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks "1")" -#Output -1590949626 -``` - -### date::sub_hours() - -Subtract number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours "1")" -#Output -1591550826 -``` - -### date::sub_minutes() - -Subtract number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes "1")" -#Output -1591554366 -``` - -### date::sub_seconds() - -Subtract number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds "1")" -#Output -1591554425 -``` - -### date::format() - -Format unix timestamp to human readable format. -If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (string): format control characters based on `date` command (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate time string. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo echo "$(date::format "1594143480")" -#Output -2020-07-07 18:38:00 -``` - -## Debug - -Functions to facilitate debugging scripts. - -### debug::print_array() - -Prints the content of array as key value pair for easier debugging. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of the array. - -#### Output on stdout - -- Formatted key value of array. - -#### Example - -```bash -array=(foo bar baz) -printf "Array\n" -printarr "array" -declare -A assoc_array -assoc_array=([foo]=bar [baz]=foobar) -printf "Assoc Array\n" -printarr "assoc_array" -#Output -Array -0 = foo -1 = bar -2 = baz -Assoc Array -baz = foobar -foo = bar -``` - -### debug::print_ansi() - -Function to print ansi escape sequence as is. -This function helps debug ansi escape sequence in text by displaying the escape codes. - -#### Arguments - -- **$1** (string): input with ansi escape sequence. - -#### Output on stdout - -- Ansi escape sequence printed in output as is. - -#### Example - -```bash -txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -debug::print_ansi "$txt" -#Output -\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -``` - -## File - -Functions for handling files. - -### file::make_temp_file() - -Create temporary file. -Function creates temporary file with random name. The temporary file will be deleted when script finishes. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp file. - -#### Output on stdout - -- file name of temporary file created. - -#### Example - -```bash -echo "$(file::make_temp_file)" -#Output -tmp.vgftzy -``` - -### file::make_temp_dir() - -Create temporary directory. -Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. - -#### Arguments - -- **$1** (string): Temporary directory prefix -- $2 string Flag to auto remove directory on exit trap (true) - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp directory. -- **2**: Missing arguments. - -#### Output on stdout - -- directory name of temporary directory created. - -#### Example - -```bash -echo "$(utility::make_temp_dir)" -#Output -tmp.rtfsxy -``` - -### file::name() - -Get only the filename from string path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- name of the file with extension. - -#### Example - -```bash -echo "$(file::name "/path/to/test.md")" -#Output -test.md -``` - -### file::basename() - -Get the basename of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- basename of the file. - -#### Example - -```bash -echo "$(file::basename "/path/to/test.md")" -#Output -test -``` - -### file::extension() - -Get the extension of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **1**: If no extension is found in the filename. -- **2**: Function missing arguments. - -#### Output on stdout - -- extension of the file. - -#### Example - -```bash -echo "$(file::extension "/path/to/test.md")" -#Output -md -``` - -### file::dirname() - -Get directory name from file path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- directory path. - -#### Example - -```bash -echo "$(file::dirname "/path/to/test.md")" -#Output -/path/to -``` - -### file::full_path() - -Get absolute path of file or directory. - -#### Arguments - -- **$1** (string): relative or absolute path to file/direcotry. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. - -#### Output on stdout - -- Absolute path to file/directory. - -#### Example - -```bash -file::full_path "../path/to/file.md" -#Output -/home/labbots/docs/path/to/file.md -``` - -### file::mime_type() - -Get mime type of provided input. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. -- **3**: If file or mimetype command not found in system. - -#### Output on stdout - -- mime type of file/directory. - -#### Example - -```bash -file::mime_type "../src/file.sh" -#Output -application/x-shellscript -``` - -### file::contains_text() - -Search if a given pattern is found in file. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. -- **$2** (string): search key or regular expression. - -#### Exit codes - -- **0**: If given search parameter is found in file. -- **1**: If search paramter not found in file. -- **2**: Function missing arguments. - -#### Example - -```bash -file::contains_text "./file.sh" "^[ @[:alpha:]]*" -file::contains_text "./file.sh" "@file" -#Output -0 -``` - -## Format - -Functions to format provided input. - -### format::human_readable_seconds() - -Format seconds to human readable format. - -#### Arguments - -- **$1** (int): number of seconds. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo "$(format::human_readable_seconds "356786")" -#Output -4 days 3 hours 6 minute(s) and 26 seconds -``` - -### format::bytes_to_human() - -Format bytes to human readable format. - -#### Arguments - -- **$1** (int): size in bytes. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted file size string. - -#### Example - -```bash -echo "$(format::bytes_to_human "2250")" -#Output -2.19 KB -``` - -### format::strip_ansi() - -Remove Ansi escape sequences from given text. - -#### Arguments - -- **$1** (string): Input text to be ansi stripped. - -#### Exit codes - -- **0**: If successful. - -#### Output on stdout - -- Ansi stripped text. - -#### Example - -```bash -format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -#Output -This is bold red text.This is green text. -``` - -### format::text_center() - -Prints the given text to centre of terminal. - -#### Arguments - -- **$1** (string): Text to be printed. -- **$2** (string): Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::text_center "This text is in centre of the terminal." "-" -``` - -### format::report() - -Format String to print beautiful report. - -#### Arguments - -- **$1** (string): Text to be printed on the left. -- **$2** (string): Text to be printed within the square brackets. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::report "Initialising mission state" "Success" -#Output -Initialising mission state ....................................................................[ Success ] -``` - -### format::trim_text_to_term() - -Trim given text to width of the terminal window. - -#### Arguments - -- **$1** (string): Text of first sentence. -- **$2** (string): Text of second sentence (optional). - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- trimmed text. - -#### Example - -```bash -format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -#Output -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -``` - -## Interaction - -Functions to enable interaction with the user. - -### interaction::prompt_yes_no() - -Prompt yes or no question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer \[yes/no\] (optional). - -#### Exit codes - -- **0**: If user responds with yes. -- **1**: If user responds with no. -- **2**: Function missing arguments. - -#### Example - -```bash -interaction::prompt_yes_no "Are you sure to proceed" "yes" -#Output -Are you sure to proceed (y/n)? [y] -``` - -### interaction::prompt_response() - -Prompt question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer (optional). - -#### Exit codes - -- **0**: If user responds with answer. -- **2**: Function missing arguments. - -#### Output on stdout - -- User entered answer to the question. - -#### Example - -```bash -interaction::prompt_response "Choose directory to install" "/home/path" -#Output -Choose directory to install? [/home/path] -``` - -## Json - -Simple json manipulation. These functions does not completely replace `jq` in any way. - -### json::get_value() - -Extract value from json based on key and position. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): id of the field to fetch. -- **$2** (int): position of value to extract.Defaults to 1.(optional) - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string value of extracted key. - -#### Example - -```bash -json::get_value "id" "1" < json_file -json::get_value "id" <<< "${json_var}" -echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -``` - -## Miscellaneous - -Set of miscellaneous helper functions. - -### misc::check_internet_connection() - -Check if internet connection is available. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script can connect to internet. -- **1**: If script cannot access internet. - -#### Example - -```bash -misc::check_internet_connection -``` - -### misc::get_pid() - -Get list of process ids based on process name. - -#### Arguments - -- **$1** (Name): of the process to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- list of process ids. - -#### Example - -```bash -misc::get_pid "chrome" -#Ouput -25951 -26043 -26528 -26561 -``` - -### misc::get_uid() - -Get user id based on username. - -#### Arguments - -- **$1** (username): to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string uid for the username. - -#### Example - -```bash -misc::get_uid "labbots" -#Ouput -1000 -``` - -### misc::generate_uuid() - -Generate random uuid. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If match successful. - -#### Output on stdout - -- random generated uuid. - -#### Example - -```bash -misc::generate_uuid -#Ouput -65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -``` - -## Operating System - -Functions to detect Operating system and version. - -### os::detect_os() - -Identify the OS the function is run on. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If OS is successfully detected. -- **1**: If unable to detect OS. - -#### Output on stdout - -- Operating system name (linux, mac or windows). - -#### Example - -```bash -os::detect_os -#Output -linux -``` - -### os::detect_linux_distro() - -Identify the distribution flavour of linux. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux distro is successfully detected. -- **1**: If unable to detect OS distro. - -#### Output on stdout - -- Linux OS distribution name (ubuntu, debian, suse, etc.,). - -#### Example - -```bash -os::detect_linux_distro -#Output -ubuntu -``` - -### os::detect_linux_version() - -Identify the Linux version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux version is successfully detected. -- **1**: If unable to detect Linux version. - -#### Output on stdout - -- Linux OS version number (18.04, 20.04, etc.,). - -#### Example - -```bash -os::detect_linux_version -#Output -20.04 -``` - -### os::detect_mac_version() - -Identify the MacOS version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If MacOS version is successfully detected. -- **1**: If unable to detect MacOS version. - -#### Output on stdout - -- MacOS version number (10.15.6, etc.,) - -#### Example - -```bash -os::detect_linux_version -#Output -10.15.7 -``` - -## String - -Functions for string operations and manipulations. - -### string::trim() - -Strip whitespace from the beginning and end of a string. - -#### Arguments - -- **$1** (string): The string to be trimmed. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The trimmed string. - -#### Example - -```bash -echo "$(string::trim " Hello World! ")" -#Output -Hello World! -``` - -### string::split() - -Split a string to array by a delimiter. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The delimiter string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns an array of strings created by splitting the string parameter by the delimiter. - -#### Example - -```bash -array=( $(string::split "a,b,c" ",") ) -printf "%s" "$(string::split "Hello!World" "!")" -#Output -Hello -World -``` - -### string::lstrip() - -Strip characters from the beginning of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::lstrip "Hello World!" "He")" -#Output -llo World! -``` - -### string::rstrip() - -Strip characters from the end of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::rstrip "Hello World!" "d!")" -#Output -Hello Worl -``` - -### string::to_lower() - -Make a string lowercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the lowercased string. - -#### Example - -```bash -echo "$(string::to_lower "HellO")" -#Output -hello -``` - -### string::to_upper() - -Make a string all uppercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the uppercased string. - -#### Example - -```bash -echo "$(string::to_upper "HellO")" -#Output -HELLO -``` - -### string::contains() - -Check whether the search string exists within the input string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::contains "Hello World!" "lo" -``` - -### string::starts_with() - -Check whether the input string starts with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::starts_with "Hello World!" "He" -``` - -### string::ends_with() - -Check whether the input string ends with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::ends_with "Hello World!" "d!" -``` - -### string::regex() - -Check whether the input string matches the given regex. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::regex "HELLO" "^[A-Z]*$" -``` - -## Terminal - -Set of useful terminal functions. - -### terminal::is_term() - -Check if script is run in terminal. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -### terminal::detect_profile() - -Detect profile rc file for zsh and bash of current script running user. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -#### Output on stdout - -- path to the profile file. - -### terminal::clear_line() - -Clear the output in terminal on the specified line number. -This function clears line only on terminal. - -#### Arguments - -- **$1** (Line): number to clear. Defaults to 1. (optional) - -#### Exit codes - -- **0**: If script is run on terminal. - -#### Output on stdout - -- clear line ansi code. - -## Validation - -Functions to perform validation on given data. - -### validation::email() - -Validate whether a given input is a valid email address or not. - -#### Arguments - -- **$1** (string): input email address to validate. - -#### Exit codes - -- **0**: If provided input is an email address. -- **1**: If provided input is not an email address. -- **2**: Function missing arguments. - -#### Example - -```bash -test='test@gmail.com' -validation::email "${test}" -echo $? -#Output -0 -``` - -### validation::ipv4() - -Validate whether a given input is a valid IP V4 address. - -#### Arguments - -- **$1** (string): input IPv4 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv4. -- **1**: If provided input is not a valid IPv4. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 4.2.2.2 - a.b.c.d - 192.168.1.1 - 0.0.0.0 - 255.255.255.255 - 255.255.255.256 - 192.168.0.1 - 192.168.0 - 1234.123.123.123 - 0.192.168.1 - ' -for ip in $ips; do - if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi - printf "%-20s: %s\n" "$ip" "$stat" -done -#Output -4.2.2.2 : good -a.b.c.d : bad -192.168.1.1 : good -0.0.0.0 : good -255.255.255.255 : good -255.255.255.256 : bad -192.168.0.1 : good -192.168.0 : bad -1234.123.123.123 : bad -0.192.168.1 : good -``` - -### validation::ipv6() - -Validate whether a given input is a valid IP V6 address. - -#### Arguments - -- **$1** (string): input IPv6 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv6. -- **1**: If provided input is not a valid IPv6. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 2001:db8:85a3:8d3:1319:8a2e:370:7348 - fe80::1ff:fe23:4567:890a - fe80::1ff:fe23:4567:890a%eth2 - 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar - fezy::1ff:fe23:4567:890a - :: - 2001:db8:: - ' -for ip in $ips; do - if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi - printf "%-50s= %s\n" "$ip" "$stat" -done -#Output -2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -fe80::1ff:fe23:4567:890a = good -fe80::1ff:fe23:4567:890a%eth2 = good -2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -fezy::1ff:fe23:4567:890a = bad -:: = good -2001:db8:: = good -``` - -### validation::alpha() - -Validate if given variable is entirely alphabetic characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is only alpha characters. -- **1**: If input contains any non alpha characters. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abcABC' -validation::alpha "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_num() - -Check if given variable contains only alpha-numeric characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is an alpha-numeric. -- **1**: If input is not an alpha-numeric. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc123' -validation::alpha_num "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_dash() - -Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is valid. -- **1**: If input the input is not valid. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc-ABC_cD' -validation::alpha_dash "${test}" -echo $? -#Output -0 -``` - -### validation::version_comparison() - -Compares version numbers and provides return based on whether the value in equal, less than or greater. - -#### Arguments - -- **$1** (string): Version number to check (eg: 1.0.1) - -#### Exit codes - -- **0**: version number is equal. -- **1**: $1 version number is greater than $2. -- **2**: $1 version number is less than $2. -- **3**: Function is missing required arguments. -- **4**: Provided input argument is in invalid format. - -#### Example - -```bash -test='abc-ABC_cD' -validation::version_comparison "12.0.1" "12.0.1" -echo $? -#Output -0 -``` - -## Variable - -Functions for handling variables. - -### variable::is_array() - -Check if given variable is array. -Pass the variable name instead of value of the variable. - -#### Arguments - -- **$1** (string): name of the variable to check. - -#### Exit codes - -- **0**: If input is array. -- **1**: If input is not an array. - -#### Example - -```bash -arr=("a" "b" "c") -variable::is_array "arr" -#Output -0 -``` - -### variable::is_numeric() - -Check if given variable is a number. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is number. -- **1**: If input is not a number. - -#### Example - -```bash -variable::is_numeric "1234" -#Output -0 -``` - -### variable::is_int() - -Check if given variable is an integer. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is an integer. -- **1**: If input is not an integer. - -#### Example - -```bash -variable::is_int "+1234" -#Output -0 -``` - -### variable::is_float() - -Check if given variable is a float. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a float. -- **1**: If input is not a float. - -#### Example - -```bash -variable::is_float "+1234.0" -#Output -0 -``` - -### variable::is_bool() - -Check if given variable is a boolean. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a boolean. -- **1**: If input is not a boolean. - -#### Example - -```bash -variable::is_bool "true" -#Output -0 -``` - -### variable::is_true() - -Check if given variable is a true. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is true. -- **1**: If input is not true. - -#### Example - -```bash -variable::is_true "true" -#Output -0 -``` - -### variable::is_false() - -Check if given variable is false. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is false. -- **1**: If input is not false. - -#### Example - -```bash -variable::is_false "false" -#Output -0 -``` - -### variable::is_empty_or_null() - -Check if given variable is empty or null. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is empty or null. -- **1**: If input is not empty or null. - -#### Example - -```bash -test='' -variable::is_empty_or_null $test -#Output -0 -``` - - - -## Inspired By - -- [Bash Bible](https://github.com/dylanaraps/pure-bash-bible) - A collection of pure bash alternatives to external processes. - -## License - -[MIT](https://github.com/labbots/google-drive-upload/blob/master/LICENSE) diff --git a/src/bash-utility/bash_utility.sh b/src/bash-utility/bash_utility.sh deleted file mode 100644 index 65411add0..000000000 --- a/src/bash-utility/bash_utility.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck disable=SC1091 -source src/array.sh -source src/string.sh -source src/variable.sh -source src/file.sh -source src/misc.sh -source src/date.sh -source src/interaction.sh -source src/check.sh -source src/format.sh -source src/collection.sh -source src/json.sh -source src/terminal.sh -source src/validation.sh -source src/debug.sh -source src/os.sh - - diff --git a/src/bash-utility/bin/bashdoc.awk b/src/bash-utility/bin/bashdoc.awk deleted file mode 100644 index 13efdaf7b..000000000 --- a/src/bash-utility/bin/bashdoc.awk +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/awk -f - -# Varibles -# style = readme or doc -# toc = true or false -BEGIN { - if (! style) { - style = "doc" - } - - if (! toc) { - toc = 0 - } - - styles["empty", "from"] = ".*" - styles["empty", "to"] = "" - - styles["h1", "from"] = ".*" - styles["h1", "to"] = "# &" - - styles["h2", "from"] = ".*" - styles["h2", "to"] = "## &" - - styles["h3", "from"] = ".*" - styles["h3", "to"] = "### &" - - styles["h4", "from"] = ".*" - styles["h4", "to"] = "#### &" - - styles["h5", "from"] = ".*" - styles["h5", "to"] = "##### &" - - styles["code", "from"] = ".*" - styles["code", "to"] = "```&" - - styles["/code", "to"] = "```" - - styles["argN", "from"] = "^(\\$[0-9]) (\\S+)" - styles["argN", "to"] = "**\\1** (\\2):" - - styles["arg@", "from"] = "^\\$@ (\\S+)" - styles["arg@", "to"] = "**...** (\\1):" - - styles["li", "from"] = ".*" - styles["li", "to"] = "- &" - - styles["i", "from"] = ".*" - styles["i", "to"] = "*&*" - - styles["anchor", "from"] = ".*" - styles["anchor", "to"] = "[&](#&)" - - styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" - styles["exitcode", "to"] = "**\\1**: \\2" - - styles["h_rule", "to"] = "---" - - styles["comment", "from"] = ".*" - styles["comment", "to"] = "" - - - output_format["readme", "h1"] = "h2" - output_format["readme", "h2"] = "h3" - output_format["readme", "h3"] = "h4" - output_format["readme", "h4"] = "h5" - - output_format["bashdoc", "h1"] = "h1" - output_format["bashdoc", "h2"] = "h2" - output_format["bashdoc", "h3"] = "h3" - output_format["bashdoc", "h4"] = "h4" - - output_format["webdoc", "h1"] = "empty" - output_format["webdoc", "h2"] = "h3" - output_format["webdoc", "h3"] = "h4" - output_format["webdoc", "h4"] = "h5" - -} - -function render(type, text) { - if((style,type) in output_format){ - type = output_format[style,type] - } - return gensub( \ - styles[type, "from"], - styles[type, "to"], - "g", - text \ - ) -} - -function render_list(item, anchor) { - return "- [" item "](#" anchor ")" -} - -function generate_anchor(text) { - # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 - text = tolower(text) - gsub(/[^[:alnum:]_ -]/, "", text) - gsub(/ /, "-", text) - return text -} - -function reset() { - has_example = 0 - has_args = 0 - has_exitcode = 0 - has_stdout = 0 - - content_desc = "" - content_example = "" - content_args = "" - content_exitcode = "" - content_seealso = "" - content_stdout = "" -} - -/^[[:space:]]*# @internal/ { - is_internal = 1 -} - -/^[[:space:]]*# @file/ { - sub(/^[[:space:]]*# @file /, "") - - filedoc = render("h1", $0) "\n" - if(style == "webdoc"){ - filedoc = filedoc render("comment", "file=" $0) "\n" - } - -} - -/^[[:space:]]*# @brief/ { - sub(/^[[:space:]]*# @brief /, "") - if(style == "webdoc"){ - filedoc = filedoc render("comment", "brief=" $0) "\n" - } - filedoc = filedoc "\n" $0 -} - -/^[[:space:]]*# @description/ { - in_description = 1 - in_example = 0 - - reset() - - docblock = "" -} - -in_description { - if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) { - if (!match(content_desc, /\n$/)) { - content_desc = content_desc "\n" - } - in_description = 0 - } else { - sub(/^[[:space:]]*# @description /, "") - sub(/^[[:space:]]*# /, "") - sub(/^[[:space:]]*#$/, "") - - content_desc = content_desc "\n" $0 - } -} - -in_example { - - if (! /^[[:space:]]*#[ ]{3}/) { - - in_example = 0 - - content_example = content_example "\n" render("/code") "\n" - } else { - sub(/^[[:space:]]*#[ ]{3}/, "") - - content_example = content_example "\n" $0 - } -} - -/^[[:space:]]*# @example/ { - in_example = 1 - content_example = content_example "\n" render("h3", "Example") - content_example = content_example "\n\n" render("code", "bash") -} - -/^[[:space:]]*# @arg/ { - if (!has_args) { - has_args = 1 - - content_args = content_args "\n" render("h3", "Arguments") "\n\n" - } - - sub(/^[[:space:]]*# @arg /, "") - - $0 = render("argN", $0) - $0 = render("arg@", $0) - - content_args = content_args render("li", $0) "\n" -} - -/^[[:space:]]*# @noargs/ { - content_args = content_args "\n" render("i", "Function has no arguments.") "\n" -} - -/^[[:space:]]*# @exitcode/ { - if (!has_exitcode) { - has_exitcode = 1 - - content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n" - } - - sub(/^[[:space:]]*# @exitcode /, "") - - $0 = render("exitcode", $0) - - content_exitcode = content_exitcode render("li", $0) "\n" -} - -/^[[:space:]]*# @see/ { - sub(/[[:space:]]*# @see /, "") - anchor = generate_anchor($0) - $0 = render_list($0, anchor) - - content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n" -} - -/^[[:space:]]*# @stdout/ { - has_stdout = 1 - - sub(/^[[:space:]]*# @stdout /, "") - - content_stdout = content_stdout "\n" render("h3", "Output on stdout") - content_stdout = content_stdout "\n\n" render("li", $0) "\n" -} - -{ - docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso - if(style == "webdoc"){ - docblock = docblock "\n" render("h_rule") "\n" - } -} - -/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example { - if (is_internal) { - is_internal = 0 - } else { - func_name = gensub(\ - /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \ - "\\3()", \ - "g" \ - ) - doc = doc "\n" render("h2", func_name) "\n" docblock - if (toc) { - url = generate_anchor(func_name) - - content_idx = content_idx "\n" "- [" func_name "](#" url ")" - } - } - - docblock = "" - reset() -} - -END { - if (filedoc != "") { - print filedoc - } - - if (toc) { - print "" - print render("h2", "Table of Contents") - print content_idx - print "" - print render("h_rule") - } - - print doc -} diff --git a/src/bash-utility/bin/generate_readme.sh b/src/bash-utility/bin/generate_readme.sh deleted file mode 100644 index 858a4b8be..000000000 --- a/src/bash-utility/bin/generate_readme.sh +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env bash - -#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh -#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh -_usage() { - printf " -Script to autogenerate markdown based on bash source code.\n -The script generates table of contents and bashdoc and update the given markdown file.\n -Usage:\n %s [options.. ]\n -Options:\n - -f | --file - Relative or absolute path to the README.md file. - -s | --sh-dir - path to the bash script source folder to generate shdocs.\n - -l | --toc-level - Minimum level of header to print in Table of Contents.\n - -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n - -w | --webdoc - Flag to indicate generation of webdoc.\n - -p | --dest-dir - Path in which wedoc files must be generated.\n - -h | --help - Display usage instructions.\n" "${0##*/}" - exit 0 -} - -_setup_arguments() { - - unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR - MINLEVEL=1 - MAXLEVEL=3 - SCRIPT_FILE="${0##*/}" - declare source="${BASH_SOURCE[0]}" - while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - source="$(readlink "$source")" - [[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located - done - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md" - SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src" - WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions" - - SHORTOPTS="whp:f:m:d:s:-:" - - while getopts "${SHORTOPTS}" OPTION; do - case "${OPTION}" in - -) - _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } - case "${OPTARG}" in - help) - _usage - ;; - file) - _check_longoptions "${!OPTIND}" - SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-level) - _check_longoptions "${!OPTIND}" - MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-depth) - _check_longoptions "${!OPTIND}" - MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - sh-dir) - _check_longoptions "${!OPTIND}" - SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - webdoc) - WEBDOC=true - ;; - dest-dir) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - '') - _usage - ;; - *) - printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - ;; - h) - _usage - ;; - f) - SOURCE_MARKDOWN="${OPTARG}" - ;; - m) - MINLEVEL="${OPTARG}" - ;; - d) - MAXLEVEL="${OPTARG}" - ;; - s) - SOURCE_SCRIPT_DIR="${OPTARG}" - ;; - w) - WEBDOC=true - ;; - p) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - :) - printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - ?) - printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - done - shift "$((OPTIND - 1))" - - if [[ -w "${SOURCE_MARKDOWN}" ]]; then - declare src_file src_extension - src_file="${SOURCE_MARKDOWN##*/}" - src_extension="${src_file##*.}" - if [[ "${src_extension,,}" != "md" ]]; then - printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 - fi - else - printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 - fi - - if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then - printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 - fi - - declare re='^[0-9]+$' - if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then - echo "error: Not a number" >&2 - exit 1 - fi - if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then - printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 - fi - - [ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}" - -} - -_setup_tempfile() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -_generate_shdoc() { - declare file - file="$(realpath "${1}")" - if [[ -s "${file}" ]]; then - awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2" - #awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md" - fi -} - -_insert_shdoc_to_file() { - declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc - shdoc_tmp_file="$1" - source_markdown="$2" - - start_shdoc="" - info_shdoc="" - end_shdoc="" - - sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then - # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file - - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" - echo -e "Updated bashdoc content to ${source_markdown} successfully\n" - - else - { - printf "%s\n" "${start_shdoc}" - cat "${shdoc_tmp_file}" - printf "%s\n" "${end_shdoc}" - } >> "${source_markdown}" - echo -e "Created bashdoc content to ${source_markdown} successfully\n" - fi -} - -_process_sh_files() { - declare shdoc_tmp_file source_script_dir source_markdown - source_markdown="${1}" - source_script_dir="${2}" - shdoc_tmp_file=$(_setup_tempfile) - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_shdoc "${line}" "${shdoc_tmp_file}" - done - _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" - rm "${shdoc_tmp_file}" - -} - -_generate_toc() { - - declare line level title anchor output counter temp_output invalid_chars - - invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" - while IFS='' read -r line || [[ -n "${line}" ]]; do - level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" - title="$(echo "${line}" | sed -E 's/^#+ //')" - [[ "${title}" = "Table of Contents" ]] && continue - - # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text - anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" - - # check new line introduced is not duplicated, if is duplicated, introduce a number at the end - temp_output=$output"$level- [$title](#$anchor)\n" - counter=1 - while true; do - nlines="$(echo -e "${temp_output}" | wc -l)" - duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)" - if [ "${nlines}" = "${duplines}" ]; then - break - fi - temp_output=$output"$level- [$title](#$anchor-$counter)\n" - counter=$((counter + 1)) - done - - output="$temp_output" - - # grep: filter header candidates to be included in toc - # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment - done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")" - - # when in toc we have two `--` quit one - echo "$output" - -} - -_insert_toc_to_file() { - - declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash - source_markdown="${1}" - toc_text="${2}" - start_toc="" - info_toc="" - end_toc="" - - toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" - # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command - utext_ampersand="id8234923000230gzz" - utext_slash="id9992384923423gzz" - toc_block="${toc_block//\&/${utext_ampersand}}" - toc_block="${toc_block//\//${utext_slash}}" - - # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 - # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches - if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then - # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace - sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" - echo -e "Updated TOC content in ${source_markdown} succesfully\n" - - else - sed -i 1i"$toc_block" "${source_markdown}" - echo -e "Created TOC in ${source_markdown} succesfully\n" - - fi - - # undo symbol replacements - sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" - sed -i "s,${utext_slash},\/,g" "${source_markdown}" - -} - -_process_toc() { - declare toc_temp_file source_markdown level toc_text - source_markdown="${1}" - - toc_temp_file=$(_setup_tempfile) - - sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" - - level=$MINLEVEL - while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do - level=$((level + 1)) - done - - MINLEVEL=${level} - toc_text=$(_generate_toc "${toc_temp_file}") - rm "${toc_temp_file}" - - _insert_toc_to_file "${source_markdown}" "${toc_text}" -} - -_generate_webdoc() { - declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file - declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc - declare webdoc_lastmod_date webdoc_lastmod_epoc - file="$(realpath "${1}")" - dest_dir="${2}" - - filename="${file##*/}" - file_basename="${filename%.*}" - dest_file_path="${dest_dir}/${file_basename}.md" - file_modified_date="$(date -r "${file}" +"%FT%T%:z")" - file_modified_date_epoc="$(date -r "${file}" +"%s")" - - start_shdoc="" - end_shdoc="" - if [[ ! -f "${dest_file_path}" ]]; then - - cat << EOF > "${dest_file_path}" ---- -title : -description : -date : ${file_modified_date} -lastmod : ${file_modified_date} ---- -${start_shdoc} -${end_shdoc} -EOF - is_new_file=true - else - is_new_file=false - webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")" - webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")" - fi - - if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then - - shdoc_tmp_file=$(_setup_tempfile) - if [[ -s "${file}" ]]; then - awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}" - fi - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}" - fi - - # Extract title and description from webdoc - title="$(sed -ne 's/-->//; s/^.*//; s/^.*/${title}/g" "${dest_file_path}" - sed -i -e "s//${description}/g" "${dest_file_path}" - else - sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}" - sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}" - - fi - - # Update the last modified timestamp in front matter - sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}" - - echo -e "Updated bashdoc content to ${dest_file_path} successfully." - rm "${shdoc_tmp_file}" - - fi -} -_process_webdoc_files() { - declare source_script_dir dest_dir - - source_script_dir="${1}" - dest_dir="${2}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_webdoc "${line}" "${dest_dir}" - done -} - -_count_library_functions() { - declare source_script_dir - source_script_dir="${1}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - { - declare -i function_count=0 count=0 - while IFS= read -r -d '' line; do - count=0 - count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}") - function_count=$((function_count + count)) - done - printf "Total library functions: %s \n" "${function_count}" - - } -} -main() { - # export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' - # set -x - trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT - set -o errexit -o noclobber -o pipefail - - _setup_arguments "${@}" - _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" - _process_toc "${SOURCE_MARKDOWN}" - - if [[ -n ${WEBDOC} ]]; then - _process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}" - fi - _count_library_functions "${SOURCE_SCRIPT_DIR}" -} - -main "${@}" diff --git a/src/bash-utility/image/bash-utility.png b/src/bash-utility/image/bash-utility.png deleted file mode 100644 index c1bfb8b556809ce645cf41ab6d4b11547df68fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32396 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 diff --git a/src/bash-utility/src/array.sh b/src/bash-utility/src/array.sh deleted file mode 100644 index 42d5883b8..000000000 --- a/src/bash-utility/src/array.sh +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env bash - -# @file Array -# @brief Functions for array operations and manipulations. - -# @description Check if item exists in the given array. -# -# @example -# array=("a" "b" "c") -# array::contains "c" ${array[@]} -# #Output -# 0 -# -# @arg $1 mixed Item to search (needle). -# @arg $2 array array to be searched (haystack). -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found in the array. -# @exitcode 2 Function missing arguments. -array::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare query="${1:-}" - shift - - for element in "${@}"; do - [[ "${element}" == "${query}" ]] && return 0 - done - - return 1 -} - -# @description Remove duplicate items from the array. -# -# @example -# array=("a" "b" "a" "c") -# printf "%s" "$(array::dedupe ${array[@]})" -# #Output -# a -# b -# c -# -# @arg $1 array Array to be deduped. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Deduplicated array. -array::dedupe() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -A arr_tmp - declare -a arr_unique - for i in "$@"; do - { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue - arr_unique+=("${i}") && arr_tmp[${i}]=x - done - printf '%s\n' "${arr_unique[@]}" -} - -# @description Check if a given array is empty. -# -# @example -# array=("a" "b" "c" "d") -# array::is_empty "${array[@]}" -# -# @arg $1 array Array to be checked. -# -# @exitcode 0 If the given array is empty. -# @exitcode 2 If the given array is not empty. -array::is_empty() { - declare -a array - local array=("$@") - if [ ${#array[@]} -eq 0 ]; then - return 0 - else - return 1 - fi -} -# @description Join array elements with a string. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s" "$(array::join "," "${array[@]}")" -# #Output -# a,b,c,d -# printf "%s" "$(array::join "" "${array[@]}")" -# #Output -# abcd -# -# @arg $1 string String to join the array elements (glue). -# @arg $2 array array to be joined with glue string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. -array::join() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare delimiter="${1}" - shift - printf "%s" "${1}" - shift - printf "%s" "${@/#/${delimiter}}" -} - -# @description Return an array with elements in reverse order. -# -# @example -# array=(1 2 3 4 5) -# printf "%s" "$(array::reverse "${array[@]}")" -# #Output -# 5 4 3 2 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The reversed array. -array::reverse() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare min=0 - declare -a array - array=("$@") - declare max=$((${#array[@]} - 1)) - - while [[ $min -lt $max ]]; do - # Swap current first and last elements - x="${array[$min]}" - array[$min]="${array[$max]}" - array[$max]="$x" - - # Move closer - ((min++, max--)) - done - printf '%s\n' "${array[@]}" -} - -# @description Returns a random item from the array. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s\n" "$(array::random_element "${array[@]}")" -# #Output -# c -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Random item out of the array. -array::random_element() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array - local array=("$@") - printf '%s\n' "${array[RANDOM % $#]}" -} - -# @description Sort an array from lowest to highest. -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# 1 -# 2 -# 4 5 -# a -# a c -# d -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout sorted array. -array::sort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort <<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Sort an array in reverse order (highest to lowest). -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# d -# a c -# a -# 4 5 -# 2 -# 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout reverse sorted array. -array::rsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort -r<<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Bubble sort an integer array from lowest to highest. -# This sort does not work on string array. -# @example -# iarr=(4 5 1 3) -# array::bsort "${iarr[@]}" -# #Output -# 1 -# 3 -# 4 -# 5 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout bubble sorted array. -array::bsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare tmp - declare arr=("$@") - for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do - for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do - if [[ ${arr[i]} -gt ${arr[j]} ]]; then - # echo $i $j ${arr[i]} ${arr[j]} - tmp=${arr[i]} - arr[i]=${arr[j]} - arr[j]=$tmp - fi - done - done - printf "%s\n" "${arr[@]}" -} - -# @description Merge two arrays. -# Pass the variable name of the array instead of value of the variable. -# @example -# a=("a" "c") -# b=("d" "c") -# array::merge "a[@]" "b[@]" -# #Output -# a -# c -# d -# c -# -# @arg $1 string variable name of first array. -# @arg $2 string variable name of second array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Merged array. -array::merge() { - [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr1=("${!1}") - declare -a arr2=("${!2}") - declare out=("${arr1[@]}" "${arr2[@]}") - printf "%s\n" "${out[@]}" -} diff --git a/src/bash-utility/src/check.sh b/src/bash-utility/src/check.sh deleted file mode 100644 index 2b7c1eb1d..000000000 --- a/src/bash-utility/src/check.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# @file Check -# @brief Helper functions. - -# @description Check if the command exists in the system. -# -# @example -# check::command_exists "tput" -# -# @arg $1 string Command name to be searched. -# -# @exitcode 0 If the command exists. -# @exitcode 1 If the command does not exist. -# @exitcode 2 Function missing arguments. -check::command_exists() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - hash "${1}" 2> /dev/null -} - -# @description Check if the script is executed with sudo privilege. -# -# @example -# check::is_sudo -# -# @noargs -# -# @exitcode 0 If the script is executed with root privilege. -# @exitcode 1 If the script is not executed with root privilege -check::is_sudo() { - if [[ $(id -u) -ne 0 ]]; then - return 1 - fi -} diff --git a/src/bash-utility/src/collection.sh b/src/bash-utility/src/collection.sh deleted file mode 100644 index 9f0e244a2..000000000 --- a/src/bash-utility/src/collection.sh +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env bash - -# @file Collection -# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -# @description Iterates over elements of collection and invokes iteratee for each element. -# Input to the function can be a pipe output, here-string or file. -# @example -# test_func(){ -# printf "print value: %s\n" "$1" -# return 0 -# } -# arr1=("a b" "c d" "a" "d") -# printf "%s\n" "${arr1[@]}" | collection::each "test_func" -# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -# #output -# print value: a b -# print value: c d -# print value: a -# print value: d -# -# @example -# # If other function from this library is already used to process the array. -# # Then following method could be used to pass the array to the function. -# out=("$(array::dedupe "${arr1[@]}")") -# collection::each "test_func" <<< "${out[@]}" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output of iteratee function. -collection::each() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - done -} - -# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "4") -# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If iteratee function fails. -# @exitcode 2 Function missing arguments. -collection::every() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return 1 - fi - - done -} - -# @description Iterates over elements of array, returning all elements where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -# #output -# 1 -# 2 -# 3 -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values matching the iteratee function. -collection::filter() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s\n" "${it}" - fi - done -} - -# @description Iterates over elements of collection, returning the first element where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("1" "2" "3" "a") -# check_a(){ -# [[ "$1" = "a" ]] -# } -# printf "%s\n" "${arr[@]}" | collection::find "check_a" -# #Output -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -# -# @stdout first array value matching the iteratee function. -collection::find() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s" "${it}" - return 0 - fi - done - - return 1 -} - -# @description Invokes the iteratee with each element passed as argument to the iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# opt=("-a" "-l") -# printf "%s\n" "${opt[@]}" | collection::invoke "ls" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output from the iteratee function. -collection::invoke() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a args=() - declare func="${1}" - while read -r it; do - args=("${args[@]}" "$it") - done - - eval "${func}" "${args[@]}" -} - -# @description Creates an array of values by running each element in array through iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3") -# add_one(){ -# i=${1} -# i=$(( i + 1 )) -# printf "%s\n" "$i" -# } -# printf "%s\n" "${arri[@]}" | collection::map "add_one" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output result of iteratee on value. -collection::map() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - declare out - - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - out="$("${func}")" - else - out="$("${func}" "$it")" - fi - - declare -i ret=$? - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - printf "%s\n" "${out}" - done -} - -# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -# #Ouput -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values not matching the iteratee function. -# @see collection::filter -collection::reject() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - echo "$it" - fi - - done -} - -# @description Checks if iteratee returns true for any element of the array. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("a" "b" "3" "a") -# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If match successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -collection::some() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - - declare -i ret=$? - - if [[ $ret -eq 0 ]]; then - return 0 - fi - done - - return 1 -} diff --git a/src/bash-utility/src/date.sh b/src/bash-utility/src/date.sh deleted file mode 100644 index 41f071792..000000000 --- a/src/bash-utility/src/date.sh +++ /dev/null @@ -1,744 +0,0 @@ -#!/usr/bin/env bash - -# @file Date -# @brief Functions for manipulating dates. - -# @description Get current time in unix timestamp. -# -# @example -# echo "$(date::now)" -# #Output -# 1591554426 -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout current timestamp. -date::now() { - declare now - now="$(date --universal +%s)" || return $? - printf "%s" "${now}" -} - -# @description convert datetime string to unix timestamp. -# -# @example -# echo "$(date::epoc "2020-07-07 18:38")" -# #Output -# 1594143480 -# -# @arg $1 string date time in any format. -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp for specified datetime. -date::epoc() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare date - date=$(date -d "${1}" +"%s") || return $? - printf "%s" "${date}" -} - -# @description Add number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days_from "1594143480")" -# #Output -# 1594229880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months_from "1594143480")" -# #Output -# 1596821880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years_from "1594143480")" -# #Output -# 1625679480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::add_weeks_from "1594143480")" -# #Output -# 1594748280 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours_from "1594143480")" -# #Output -# 1594147080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes_from "1594143480")" -# #Output -# 1594143540 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds_from "1594143480")" -# #Output -# 1594143481 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days "1")" -# #Output -# 1591640826 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months "1")" -# #Output -# 1594146426 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years "1")" -# #Output -# 1623090426 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_weeks "1")" -# #Output -# 1592159226 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours "1")" -# #Output -# 1591558026 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes "1")" -# #Output -# 1591554486 -# -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds "1")" -# #Output -# 1591554427 -# -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days_from "1594143480")" -# #Output -# 1594057080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months_from "1594143480")" -# #Output -# 1591551480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years_from "1594143480")" -# #Output -# 1562521080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks_from "1594143480")" -# #Output -# 1593538680 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours_from "1594143480")" -# #Output -# 1594139880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes_from "1594143480")" -# #Output -# 1594143420 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds_from "1594143480")" -# #Output -# 1594143479 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days "1")" -# #Output -# 1588876026 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months "1")" -# #Output -# 1559932026 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years "1")" -# #Output -# 1591468026 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks "1")" -# #Output -# 1590949626 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours "1")" -# #Output -# 1591550826 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes "1")" -# #Output -# 1591554366 -# -# @arg $1 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds "1")" -# #Output -# 1591554425 -# -# @arg $1 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Format unix timestamp to human readable format. -# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. -# -# @example -# echo echo "$(date::format "1594143480")" -# #Output -# 2020-07-07 18:38:00 -# -# @arg $1 int unix timestamp. -# @arg $2 string format control characters based on `date` command (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate time string. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -date::format() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp format out - timestamp="${1}" - format="${2:-"%F %T"}" - out="$(date -d "@${timestamp}" +"${format}")" || return $? - printf "%s" "${out}" - -} diff --git a/src/bash-utility/src/debug.sh b/src/bash-utility/src/debug.sh deleted file mode 100644 index 82828734f..000000000 --- a/src/bash-utility/src/debug.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash - -# @file Debug -# @brief Functions to facilitate debugging scripts. - -# @description Prints the content of array as key value pair for easier debugging. -# Pass the variable name of the array instead of value of the variable. -# @example -# array=(foo bar baz) -# printf "Array\n" -# printarr "array" -# declare -A assoc_array -# assoc_array=([foo]=bar [baz]=foobar) -# printf "Assoc Array\n" -# printarr "assoc_array" -# #Output -# Array -# 0 = foo -# 1 = bar -# 2 = baz -# Assoc Array -# baz = foobar -# foo = bar -# -# @arg $1 string variable name of the array. -# -# @stdout Formatted key value of array. -debug::print_array() { - declare -n __arr="$1" - for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done -} - -# @description Function to print ansi escape sequence as is. -# This function helps debug ansi escape sequence in text by displaying the escape codes. -# -# @example -# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -# debug::print_ansi "$txt" -# #Output -# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -# -# @arg $1 string input with ansi escape sequence. -# -# @stdout Ansi escape sequence printed in output as is. -debug::print_ansi() { - #echo $(tr -dc '[:print:]'<<<$1) - printf "%s\n" "${1//$'\e'/\\e}" - -} diff --git a/src/bash-utility/src/file.sh b/src/bash-utility/src/file.sh deleted file mode 100644 index 71afd8476..000000000 --- a/src/bash-utility/src/file.sh +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env bash - -# @file File -# @brief Functions for handling files. - -# @description Create temporary file. -# Function creates temporary file with random name. The temporary file will be deleted when script finishes. -# -# @example -# echo "$(file::make_temp_file)" -# #Output -# tmp.vgftzy -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp file. -# -# @stdout file name of temporary file created. -file::make_temp_file() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -# @description Create temporary directory. -# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. -# -# @example -# echo "$(utility::make_temp_dir)" -# #Output -# tmp.rtfsxy -# -# @arg $1 string Temporary directory prefix -# @arg $2 string Flag to auto remove directory on exit trap (true) -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp directory. -# @exitcode 2 Missing arguments. -# -# @stdout directory name of temporary directory created. -file::make_temp_dir() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare temp_dir prefix="${1}" trap_rm="${2}" - temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") - if [[ -n "${trap_rm}" ]]; then - trap 'rm -rf "${temp_dir}"' EXIT - fi - printf "%s" "${temp_dir}" -} - -# @description Get only the filename from string path. -# -# @example -# echo "$(file::name "/path/to/test.md")" -# #Output -# test.md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout name of the file with extension. -file::name() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf "%s" "${1##*/}" -} - -# @description Get the basename of file from file name. -# -# @example -# echo "$(file::basename "/path/to/test.md")" -# #Output -# test -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout basename of the file. -file::basename() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare file basename - file="${1##*/}" - basename="${file%.*}" - - printf "%s" "${basename}" -} - -# @description Get the extension of file from file name. -# -# @example -# echo "$(file::extension "/path/to/test.md")" -# #Output -# md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 1 If no extension is found in the filename. -# @exitcode 2 Function missing arguments. -# -# @stdout extension of the file. -file::extension() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare file extension - file="${1##*/}" - extension="${file##*.}" - [[ "${file}" = "${extension}" ]] && return 1 - - printf "%s" "${extension}" -} - -# @description Get directory name from file path. -# -# @example -# echo "$(file::dirname "/path/to/test.md")" -# #Output -# /path/to -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout directory path. -file::dirname() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare tmp=${1:-.} - - [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } - tmp="${tmp%%"${tmp##*[!/]}"}" - - [[ ${tmp} != */* ]] && { printf '.\n' && return; } - tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" - - printf '%s' "${tmp:-/}" -} - -# @description Get absolute path of file or directory. -# -# @example -# file::full_path "../path/to/file.md" -# #Output -# /home/labbots/docs/path/to/file.md -# -# @arg $1 string relative or absolute path to file/direcotry. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# -# @stdout Absolute path to file/directory. -file::full_path() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare input="${1}" - if [[ -f ${input} ]]; then - printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" - elif [[ -d ${input} ]]; then - printf "%s\n" "$(cd "${input}" && pwd)" - else - return 1 - fi -} - -# @description Get mime type of provided input. -# -# @example -# file::mime_type "../src/file.sh" -# #Output -# application/x-shellscript -# -# @arg $1 string relative or absolute path to file/directory. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# @exitcode 3 If file or mimetype command not found in system. -# -# @stdout mime type of file/directory. -file::mime_type() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare mime_type - if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then - if type -p mimetype &> /dev/null; then - mime_type=$(mimetype --output-format %m "${1}") - elif type -p file &> /dev/null; then - mime_type=$(file --brief --mime-type "${1}") - else - return 3 - fi - else - return 1 - fi - printf "%s" "${mime_type}" -} - -# @description Search if a given pattern is found in file. -# -# @example -# file::contains_text "./file.sh" "^[ @[:alpha:]]*" -# file::contains_text "./file.sh" "@file" -# #Output -# 0 -# -# @arg $1 string relative or absolute path to file/directory. -# @arg $2 string search key or regular expression. -# -# @exitcode 0 If given search parameter is found in file. -# @exitcode 1 If search paramter not found in file. -# @exitcode 2 Function missing arguments. -file::contains_text() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - declare -r file="$1" - declare -r text="$2" - grep -q "$text" "$file" -} diff --git a/src/bash-utility/src/format.sh b/src/bash-utility/src/format.sh deleted file mode 100644 index 7dceb1d59..000000000 --- a/src/bash-utility/src/format.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash - -# @file Format -# @brief Functions to format provided input. - -# @internal -# @description Initialisation script when the code is sourced. -# -# @noargs -__init(){ -_check_terminal_window_size -} - -# @internal -# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. -# -# @noargs -_check_terminal_window_size() { - shopt -s checkwinsize && (: && :) - trap 'shopt -s checkwinsize; (:;:)' SIGWINCH -} -# @description Format seconds to human readable format. -# -# @example -# echo "$(format::human_readable_seconds "356786")" -# #Output -# 4 days 3 hours 6 minute(s) and 26 seconds -# -# @arg $1 int number of seconds. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -format::human_readable_seconds() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare T="${1}" - declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" - [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" - [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" - [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" - [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' - printf '%d seconds\n' "${SEC}" -} - -# @description Format bytes to human readable format. -# -# @example -# echo "$(format::bytes_to_human "2250")" -# #Output -# 2.19 KB -# -# @arg $1 int size in bytes. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted file size string. -format::bytes_to_human() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) - while ((b > 1024)); do - d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" - b=$((b / 1024)) && ((s++)) - done - printf "%s\n" "${b}${d} ${S[${s}]}" -} - -# @description Remove Ansi escape sequences from given text. -# -# @example -# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -# #Output -# This is bold red text.This is green text. -# -# @arg $1 string Input text to be ansi stripped. -# -# @exitcode 0 If successful. -# -# @stdout Ansi stripped text. -format::strip_ansi() { - declare tmp esc tpa re - tmp="${1}" - esc=$(printf "\x1b") - tpa=$(printf "\x28") - re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" - while [[ "${tmp}" =~ $re ]]; do - tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" - done - printf "%s" "${tmp}" -} - -# @description Prints the given text to centre of terminal. -# -# @example -# format::text_center "This text is in centre of the terminal." "-" -# -# @arg $1 string Text to be printed. -# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::text_center() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare input="${1}" symbol="${2:- }" filler out no_ansi_out - no_ansi_out=$(format::strip_ansi "$input") - declare -i str_len=${#no_ansi_out} - declare -i filler_len="$(((COLUMNS - str_len) / 2))" - - [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" - for ((i = 0; i < filler_len; i++)); do - filler+="${symbol}" - done - - out="${filler}${input}${filler}" - [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" - printf "%s" "${out}" -} - -# @description Format String to print beautiful report. -# -# @example -# format::report "Initialising mission state" "Success" -# #Output -# Initialising mission state ....................................................................[ Success ] -# -# @arg $1 string Text to be printed on the left. -# @arg $2 string Text to be printed within the square brackets. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::report() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare symbol="." to_print y hl hlout out - declare input1="${1} " input2="${2}" - input2="[ $input2 ]" - to_print="$((COLUMNS * 60 / 100))" - y=$(( to_print - ( ${#input1} + ${#input2} ) )) - hl="$(printf '%*s' $y '')" - hlout=${hl// /${symbol}} - out="${input1}${hlout}${input2}" - printf "%s\n" "${out}" -} - -# @description Trim given text to width of the terminal window. -# -# @example -# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -# #Output -# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -# -# @arg $1 string Text of first sentence. -# @arg $2 string Text of second sentence (optional). -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout trimmed text. -format::trim_text_to_term() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare to_print out input1="$1" input2="$2" - if [[ $# = 1 ]]; then - to_print="$((COLUMNS * 93 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } - else - to_print="$((COLUMNS * 40 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } - to_print="$((COLUMNS * 53 / 100))" - { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } - fi - printf "%s" "$out" -} - -__init diff --git a/src/bash-utility/src/interaction.sh b/src/bash-utility/src/interaction.sh deleted file mode 100644 index 910b60e1c..000000000 --- a/src/bash-utility/src/interaction.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# @file Interaction -# @brief Functions to enable interaction with the user. - -# @description Prompt yes or no question to the user. -# -# @example -# interaction::prompt_yes_no "Are you sure to proceed" "yes" -# #Output -# Are you sure to proceed (y/n)? [y] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer \[yes/no\] (optional). -# -# @exitcode 0 If user responds with yes. -# @exitcode 1 If user responds with no. -# @exitcode 2 Function missing arguments. -interaction::prompt_yes_no() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare def_arg response - def_arg="" - response="" - - case "${2}" in - [yY] | [yY][eE][sS]) - def_arg=y - ;; - [nN] | [nN][oO]) - def_arg=n - ;; - esac - - while :; do - printf "%s (y/n)? " "${1}" - [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -z "${response}" ]] && response="${def_arg}" - - case "${response}" in - [yY] | [yY][eE][sS]) - response=y - break - ;; - [nN] | [nN][oO]) - response=n - break - ;; - *) - response="" - ;; - esac - done - - [[ "${response}" = 'y' ]] && return 0 || return 1 -} - -# @description Prompt question to the user. -# -# @example -# interaction::prompt_response "Choose directory to install" "/home/path" -# #Output -# Choose directory to install? [/home/path] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer (optional). -# -# @exitcode 0 If user responds with answer. -# @exitcode 2 Function missing arguments. -# -# @stdout User entered answer to the question. -interaction::prompt_response() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare def_arg response - response="" - def_arg="${2}" - - while :; do - printf "%s ? " "${1}" - [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -n "${response}" ]] && break - - if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then - response="${def_arg}" - break - fi - done - - [[ "${response}" = "-" ]] && response="" - - printf "%s" "${response}" -} diff --git a/src/bash-utility/src/json.sh b/src/bash-utility/src/json.sh deleted file mode 100644 index 73476618e..000000000 --- a/src/bash-utility/src/json.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# @file Json -# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. - -# @description Extract value from json based on key and position. -# Input to the function can be a pipe output, here-string or file. -# @example -# json::get_value "id" "1" < json_file -# json::get_value "id" <<< "${json_var}" -# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -# -# @arg $1 string id of the field to fetch. -# @arg $2 int position of value to extract.Defaults to 1.(optional) -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string value of extracted key. -json::get_value() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare LC_ALL=C num="${2:-1}" - grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p -} diff --git a/src/bash-utility/src/misc.sh b/src/bash-utility/src/misc.sh deleted file mode 100644 index fca9a75bf..000000000 --- a/src/bash-utility/src/misc.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash - -# @file Miscellaneous -# @brief Set of miscellaneous helper functions. - -# @internal -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -_is_terminal() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Check if internet connection is available. -# -# @example -# misc::check_internet_connection -# -# @noargs -# -# @exitcode 0 If script can connect to internet. -# @exitcode 1 If script cannot access internet. -misc::check_internet_connection() { - declare check_internet - if _is_terminal; then - check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" - else - check_internet="$(curl --compressed -Is google.com -m 10)" - fi - if [[ -z ${check_internet} ]]; then - return 1 - fi -} - -# @description Get list of process ids based on process name. -# -# @example -# misc::get_pid "chrome" -# #Ouput -# 25951 -# 26043 -# 26528 -# 26561 -# -# @arg $1 Name of the process to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout list of process ids. -misc::get_pid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - pgrep "${1}" -} - -# @description Get user id based on username. -# -# @example -# misc::get_uid "labbots" -# #Ouput -# 1000 -# -# @arg $1 username to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string uid for the username. -misc::get_uid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - user_id=$(id "${1}" 2> /dev/null) - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - printf "No user found with username: %s" "${1}\n" - return 1 - fi - - printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' - - unset user_id -} - -# @description Generate random uuid. -# -# @example -# misc::generate_uuid -# #Ouput -# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -# -# @noargs -# -# @exitcode 0 If match successful. -# -# @stdout random generated uuid. -misc::generate_uuid() { - C="89ab" - - for ((N=0;N<16;++N)); do - B="$((RANDOM%256))" - - case "$N" in - 6) printf '4%x' "$((B%16))" ;; - 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; - - 3|5|7|9) - printf '%02x-' "$B" - ;; - - *) - printf '%02x' "$B" - ;; - esac - done - - printf '\n' -} diff --git a/src/bash-utility/src/os.sh b/src/bash-utility/src/os.sh deleted file mode 100644 index 5cfecfc78..000000000 --- a/src/bash-utility/src/os.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash - -# @file Operating System -# @brief Functions to detect Operating system and version. - -# @description Identify the OS the function is run on. -# -# @noargs -# -# @example -# os::detect_os -# #Output -# linux -# -# @exitcode 0 If OS is successfully detected. -# @exitcode 1 If unable to detect OS. -# -# @stdout Operating system name (linux, mac or windows). -os::detect_os() { - declare uname os - uname=$(command -v uname) - - case $("${uname}" | tr '[:upper:]' '[:lower:]') in - linux*) - os="linux" - ;; - darwin*) - os="mac" - ;; - msys* | cygwin* | mingw* | nt | win*) - # or possible 'bash on windows' - os="windows" - ;; - *) - return 1 - ;; - esac - printf "%s" "${os}" -} - -# @description Identify the distribution flavour of linux. -# -# @noargs -# -# @example -# os::detect_linux_distro -# #Output -# ubuntu -# @exitcode 0 If Linux distro is successfully detected. -# @exitcode 1 If unable to detect OS distro. -# -# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). -os::detect_linux_distro() { - declare distro - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro="${NAME}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro=$(lsb_release -si) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro="${DISTRIB_ID}" - elif [[ -f /etc/debian_version ]]; then - # Older Debian/Ubuntu/etc. - distro="debian" - elif [[ -f /etc/SuSe-release ]]; then - # Older SuSE/etc. - distro="suse" - elif [[ -f /etc/redhat-release ]]; then - # Older Red Hat, CentOS, etc. - distro="redhat" - else - return 1 - fi - printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' -} - -# @description Identify the Linux version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 20.04 -# -# @exitcode 0 If Linux version is successfully detected. -# @exitcode 1 If unable to detect Linux version. -# -# @stdout Linux OS version number (18.04, 20.04, etc.,). -os::detect_linux_version() { - declare distro_version - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro_version="${VERSION_ID}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro_version=$(lsb_release -sr) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro_version="${DISTRIB_RELEASE}" - else - return 1 - fi - printf "%s" "${distro_version}" -} - -# @description Identify the MacOS version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 10.15.7 -# @exitcode 0 If MacOS version is successfully detected. -# @exitcode 1 If unable to detect MacOS version. -# -# @stdout MacOS version number (10.15.6, etc.,) -os::detect_mac_version() { - if [[ "$(os::detect_os)" = "mac" ]]; then - declare mac_version - mac_version="$(sw_vers -productVersion)" - printf "%s" "${mac_version}" - else - return 1 - fi -} diff --git a/src/bash-utility/src/string.sh b/src/bash-utility/src/string.sh deleted file mode 100644 index aa522cb55..000000000 --- a/src/bash-utility/src/string.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env bash - -# @file String -# @brief Functions for string operations and manipulations. - -# @description Strip whitespace from the beginning and end of a string. -# -# @example -# echo "$(string::trim " Hello World! ")" -# #Output -# Hello World! -# -# @arg $1 string The string to be trimmed. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The trimmed string. -string::trim() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - : "${1#"${1%%[![:space:]]*}"}" - : "${_%"${_##*[![:space:]]}"}" - printf '%s\n' "$_" -} - -# @description Split a string to array by a delimiter. -# -# @example -# array=( $(string::split "a,b,c" ",") ) -# printf "%s" "$(string::split "Hello!World" "!")" -# #Output -# Hello -# World -# -# @arg $1 string The input string. -# @arg $2 string The delimiter string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. -string::split() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr=() - IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" - printf '%s\n' "${arr[@]}" -} - -# @description Strip characters from the beginning of a string. -# -# @example -# echo "$(string::lstrip "Hello World!" "He")" -# #Output -# llo World! -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::lstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1##$2}" -} - -# @description Strip characters from the end of a string. -# -# @example -# echo "$(string::rstrip "Hello World!" "d!")" -# #Output -# Hello Worl -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::rstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1%%$2}" -} - -# @description Make a string lowercase. -# -# @example -# echo "$(string::to_lower "HellO")" -# #Output -# hello -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the lowercased string. -string::to_lower() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1,,}" - else - printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' - fi -} - -# @description Make a string all uppercase. -# -# @example -# echo "$(string::to_upper "HellO")" -# #Output -# HELLO -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the uppercased string. -string::to_upper() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1^^}" - else - printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' - fi -} - -# @description Check whether the search string exists within the input string. -# -# @example -# string::contains "Hello World!" "lo" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_contains hello he - [[ "${1}" == *${2}* ]] -} - -# @description Check whether the input string starts with key string. -# -# @example -# string::starts_with "Hello World!" "He" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::starts_with() { - # Usage: string_starts_with hello he - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - [[ "${1}" == ${2}* ]] -} - -# @description Check whether the input string ends with key string. -# -# @example -# string::ends_with "Hello World!" "d!" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::ends_with() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_ends_wit hello lo - [[ "${1}" == *${2} ]] -} - -# @description Check whether the input string matches the given regex. -# -# @example -# string::regex "HELLO" "^[A-Z]*$" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::regex() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${1} =~ ${2} ]]; then - return 0 - else - return 1 - fi - -} diff --git a/src/bash-utility/src/terminal.sh b/src/bash-utility/src/terminal.sh deleted file mode 100644 index d73331d7a..000000000 --- a/src/bash-utility/src/terminal.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# @file Terminal -# @brief Set of useful terminal functions. - -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -terminal::is_term() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Detect profile rc file for zsh and bash of current script running user. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -# -# @stdout path to the profile file. -terminal::detect_profile() { - declare CURRENT_SHELL="${SHELL##*/}" - case "${CURRENT_SHELL}" in - 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; - 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; - *) if [[ -f "${HOME}/.profile" ]]; then - DETECTED_PROFILE="${HOME}/.profile" - else - printf "No compaitable shell file\n" && exit 1 - fi ;; - esac - printf "%s\n" "${DETECTED_PROFILE}" -} - -# @description Clear the output in terminal on the specified line number. -# This function clears line only on terminal. -# -# @arg $1 Line number to clear. Defaults to 1. (optional) -# -# @exitcode 0 If script is run on terminal. -# -# @stdout clear line ansi code. -terminal::clear_line() { - if terminal::is_term; then - declare line=${1:-1} - printf "\033[%sA\033[2K" "${line}" - fi -} diff --git a/src/bash-utility/src/validation.sh b/src/bash-utility/src/validation.sh deleted file mode 100644 index 37fde1cbc..000000000 --- a/src/bash-utility/src/validation.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env bash - -# @file Validation -# @brief Functions to perform validation on given data. - -# @description Validate whether a given input is a valid email address or not. -# -# @example -# test='test@gmail.com' -# validation::email "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string input email address to validate. -# -# @exitcode 0 If provided input is an email address. -# @exitcode 1 If provided input is not an email address. -# @exitcode 2 Function missing arguments. -validation::email() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare email_re - email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" - [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 -} - -# @description Validate whether a given input is a valid IP V4 address. -# -# @example -# ips=' -# 4.2.2.2 -# a.b.c.d -# 192.168.1.1 -# 0.0.0.0 -# 255.255.255.255 -# 255.255.255.256 -# 192.168.0.1 -# 192.168.0 -# 1234.123.123.123 -# 0.192.168.1 -# ' -# for ip in $ips; do -# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi -# printf "%-20s: %s\n" "$ip" "$stat" -# done -# #Output -# 4.2.2.2 : good -# a.b.c.d : bad -# 192.168.1.1 : good -# 0.0.0.0 : good -# 255.255.255.255 : good -# 255.255.255.256 : bad -# 192.168.0.1 : good -# 192.168.0 : bad -# 1234.123.123.123 : bad -# 0.192.168.1 : good -# -# @arg $1 string input IPv4 address. -# -# @exitcode 0 If provided input is a valid IPv4. -# @exitcode 1 If provided input is not a valid IPv4. -# @exitcode 2 Function missing arguments. -validation::ipv4() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare ip="${1}" - declare IFS=. - # shellcheck disable=SC2206 - declare -a a=($ip) - [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 - # Test values of quads - declare quad - for quad in {0..3}; do - [[ "${a[$quad]}" -gt 255 ]] && return 1 - done - return 0 -} - -# @description Validate whether a given input is a valid IP V6 address. -# -# @example -# ips=' -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 -# fe80::1ff:fe23:4567:890a -# fe80::1ff:fe23:4567:890a%eth2 -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar -# fezy::1ff:fe23:4567:890a -# :: -# 2001:db8:: -# ' -# for ip in $ips; do -# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi -# printf "%-50s= %s\n" "$ip" "$stat" -# done -# #Output -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -# fe80::1ff:fe23:4567:890a = good -# fe80::1ff:fe23:4567:890a%eth2 = good -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -# fezy::1ff:fe23:4567:890a = bad -# :: = good -# 2001:db8:: = good -# -# @arg $1 string input IPv6 address. -# -# @exitcode 0 If provided input is a valid IPv6. -# @exitcode 1 If provided input is not a valid IPv6. -# @exitcode 2 Function missing arguments. -validation::ipv6() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare ip="${1}" - declare 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]))$" - - [[ "${ip}" =~ $re ]] && return 0 || return 1 -} - -# @description Validate if given variable is entirely alphabetic characters. -# -# @example -# test='abcABC' -# validation::alpha "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is only alpha characters. -# @exitcode 1 If input contains any non alpha characters. -# @exitcode 2 Function missing arguments. -validation::alpha() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable contains only alpha-numeric characters. -# -# @example -# test='abc123' -# validation::alpha_num "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is an alpha-numeric. -# @exitcode 1 If input is not an alpha-numeric. -# @exitcode 2 Function missing arguments. -validation::alpha_num() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alnum:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. -# -# @example -# test='abc-ABC_cD' -# validation::alpha_dash "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is valid. -# @exitcode 1 If input the input is not valid. -# @exitcode 2 Function missing arguments. -validation::alpha_dash() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]_-]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. -# -# @arg $1 string Version number to check (eg: 1.0.1) -# $arg $2 string Version number to check (eg: 1.0.1) -# -# @example -# test='abc-ABC_cD' -# validation::version_comparison "12.0.1" "12.0.1" -# echo $? -# #Output -# 0 -# -# @exitcode 0 version number is equal. -# @exitcode 1 $1 version number is greater than $2. -# @exitcode 2 $1 version number is less than $2. -# @exitcode 3 Function is missing required arguments. -# @exitcode 4 Provided input argument is in invalid format. -validation::version_comparison() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 - - declare regex="^[.0-9]*$" - ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 - ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 - - if [[ "$1" == "$2" ]]; then - return 0 - fi - declare IFS=. - declare -a ver1 ver2 - read -r -a ver1 <<<"${1}" - read -r -a ver2 <<<"${2}" - # fill empty fields in ver1 with zeros - for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do - ver1[i]=0 - done - for ((i = 0; i < ${#ver1[@]}; i++)); do - if [[ -z ${ver2[i]} ]]; then - # fill empty fields in ver2 with zeros - ver2[i]=0 - fi - if ((10#${ver1[i]} > 10#${ver2[i]})); then - return 1 - fi - if ((10#${ver1[i]} < 10#${ver2[i]})); then - return 2 - fi - done - return 0 -} diff --git a/src/bash-utility/src/variable.sh b/src/bash-utility/src/variable.sh deleted file mode 100644 index 67e9fab55..000000000 --- a/src/bash-utility/src/variable.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash - -# @file Variable -# @brief Functions for handling variables. - -# @description Check if given variable is array. -# Pass the variable name instead of value of the variable. -# -# @example -# arr=("a" "b" "c") -# variable::is_array "arr" -# #Output -# 0 -# -# @arg $1 string name of the variable to check. -# -# @exitcode 0 If input is array. -# @exitcode 1 If input is not an array. -variable::is_array() { - if [[ -z "${1}" ]]; then - return 1 - else - declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 - fi - return 1 -} - -# @description Check if given variable is a number. -# -# @example -# variable::is_numeric "1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is number. -# @exitcode 1 If input is not a number. -variable::is_numeric() { - declare re='^[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is an integer. -# -# @example -# variable::is_int "+1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is an integer. -# @exitcode 1 If input is not an integer. -variable::is_int() { - declare re='^[+-]?[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a float. -# -# @example -# variable::is_float "+1234.0" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a float. -# @exitcode 1 If input is not a float. -variable::is_float() { - declare re='^[+-]?[0-9]+.?[0-9]*$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a boolean. -# -# @example -# variable::is_bool "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a boolean. -# @exitcode 1 If input is not a boolean. -variable::is_bool() { - [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 -} - -# @description Check if given variable is a true. -# -# @example -# variable::is_true "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is true. -# @exitcode 1 If input is not true. -variable::is_true() { - [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 -} - -# @description Check if given variable is false. -# -# @example -# variable::is_false "false" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is false. -# @exitcode 1 If input is not false. -variable::is_false() { - [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 -} - -# @description Check if given variable is empty or null. -# -# @example -# test='' -# variable::is_empty_or_null $test -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is empty or null. -# @exitcode 1 If input is not empty or null. -variable::is_empty_or_null() { - [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 -} diff --git a/src/configng/README.md b/src/configng/README.md deleted file mode 100644 index 1653df4c9..000000000 --- a/src/configng/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# configng -This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility) -embedded in this project. This allows for functional programming in Bash and also modernizes -the monolithic nature of armbian-config. Error handling and validation are also included. -The idea is to provide an API in Bash that can be called from a TUI, GUI or CLI. Please -follow the coding standards which follow Bash Utility functions. - -Why Bash? Well, because it's going to be in every distribution. Striped down distributions -may not include Python, C/C++, etc. build/runtime environments - -## Quick start -* `sudo apt install git` -* `cd ~/` -* `git clone https://github.com/armbian/configng.git` -* `cd ~/configng/test` -* `sudo ./cpu_test.sh` -If all goes well you should see all the functions in cpu.sh called and output diaplayed. - -## Coding standards -[Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, -but fundementally look at the code in Bash Utility: -``` -# @description Strip characters from the beginning of a string. -# -# @example -# echo "$(string::lstrip "Hello World!" "He")" -# #Output -# llo World! -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::lstrip() { -[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 -printf '%s\n' "${1##$2}" -} -``` - -Functions should follow filename::func_name style. Then you can tell just from the name which -file the function is located in. Return codes should also follow a similar pattern: -* 0 Successful -* 1 Not found -* 2 Function missing arguments -* 3-255 all other errors - -Validate values: -``` -# Validate minimum frequency is <= maximum frequency -[ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 -``` - -Return values should use stdout: -``` -# Return value -printf '%s\n' "$(cat $file)" -``` - -Only use sudo when needed and never run as root! diff --git a/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md b/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md deleted file mode 100644 index 461c926b9..000000000 --- a/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at - -For answers to common questions about this code of conduct, see - - -[homepage]: https://www.contributor-covenant.org diff --git a/src/configng/functions/bash-utility-master/CONTRIBUTING.md b/src/configng/functions/bash-utility-master/CONTRIBUTING.md deleted file mode 100644 index aa401df88..000000000 --- a/src/configng/functions/bash-utility-master/CONTRIBUTING.md +++ /dev/null @@ -1,129 +0,0 @@ -# Contributing to Bash-Utility - -:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: -The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. - -## Table of Contents -- [Code Contributions](#code-contributions) -- [Code Guidelines](#code-guidelines) - - [Styleguide](#styleguide) - - [Bashdoc guideline](#bashdoc-guideline) -- [Documentation](#documentation) -- [Commit Guidelines](#commit-guidelines) -- [Pull Request Guidelines](#pull-request-guidelines) -- [Contact](#contact) - -## Code Contributions - -Great, the more, the merrier. - -Sane code contributions are always welcome, whether to the code or documentation. - -Before making a pull request, make sure to follow below guidelines: - -### Code Guidelines - -#### Styleguide - -- Variable names must be meaningful and self-documenting. -- Long variable names must be structured by underscores to improve legibility. -- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE) -- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data) -- Variable names can be alphanumeric with underscores. No special characters in variable names. -- Variables name must not start with number. -- Variables within function must be declared. So the scope of variable is restricted to the function. -- Avoid accessing global variables within functions. -- Function names must be all lower case with underscores to seperate words (snake_case). -- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`) -- Try using bash builtins and string substitution as much as possible. -- Use printf everywhere instead of echo. -- Before adding a new logic, be sure to check the existing code. -- Make sure to add the function in appropriate section based on its operation. -- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command: - - ```shell - shfmt upload.sh - ``` - - The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags. - You can also install shfmt for various editors, refer their repo for information. - Note: This is strictly necessary to maintain consistency, do not skip. - -- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason. - -#### Bashdoc guideline - -The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is -properly generated by the generator. - -Follow the below bashdoc template to add function introductory comment. - -```bash -# @description Multiline description goes here and -# there -# -# @example -# sample::function a b c -# echo 123 -# -# @arg $1 string Some arg. -# @arg $2 any Rest of arguments. -# -# @noargs -# -# @exitcode 0 If successfull. -# @exitcode >0 On failure -# @exitcode 5 On some error. -# -# @stdout Path to something. -# -# @see sample::other_function(() -sample::function() { -} -``` - -- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used. -- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment. -- Make sure to document the exitcode emitted by the function. -- If the function is similar to other function add a reference to function using @see tag. - -### Documentation - -- Refrain from making unnecessary newlines or whitespace. -- Use pure markdown as much as possible, html is accepted but shouldn't be a priority. -- The markdown must pass RemarkLint checks. -- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc. - - ```bash - ./bin/generate_readme.sh -f README.md -s src/ - ``` - -### Commit Guidelines - -It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted. - -It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message. - -The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation. - -Before committing check for unnecessary whitespace with `git diff --check`. - -### Pull Request Guidelines - -The following guidelines will increase the likelihood that your pull request will get accepted: - -- Follow the commit and code guidelines. -- Keep the patches on topic and focused. -- Try to avoid unnecessary formatting and clean-up where reasonable. - -A pull request should contain the following: - -- At least one commit (all of which should follow the Commit Guidelines). -- Title that summarises the issue/feature. -- Description that briefly summarises the changes. - -After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread. - -## Contact - -For further inquiries, you can contact the developer by opening an issue on the repository. diff --git a/src/configng/functions/bash-utility-master/LICENSE b/src/configng/functions/bash-utility-master/LICENSE deleted file mode 100644 index 99dd0836b..000000000 --- a/src/configng/functions/bash-utility-master/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 labbots - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/configng/functions/bash-utility-master/README.md b/src/configng/functions/bash-utility-master/README.md deleted file mode 100644 index 9bc1f9484..000000000 --- a/src/configng/functions/bash-utility-master/README.md +++ /dev/null @@ -1,3026 +0,0 @@ -

Bash Utility

- -

-Stars -License - -

-

-Gh-pages Status -Website -

-

-Total number of Library functions -

-

- -

-Bash library which provides utility functions and helpers for functional programming in Bash. - -Detailed documentation is available at - - - -## Table of Contents - -- [Installation](#installation) - - [Method 1 - Git Submodules](#method-1---git-submodules) - - [Method 2 - Git Clone](#method-2---git-clone) - - [Method 3 - Direct Download](#method-3---direct-download) -- [Usage](#usage) -- [Array](#array) - - [array::contains()](#arraycontains) - - [array::dedupe()](#arraydedupe) - - [array::is_empty()](#arrayis_empty) - - [array::join()](#arrayjoin) - - [array::reverse()](#arrayreverse) - - [array::random_element()](#arrayrandom_element) - - [array::sort()](#arraysort) - - [array::rsort()](#arrayrsort) - - [array::bsort()](#arraybsort) - - [array::merge()](#arraymerge) -- [Check](#check) - - [check::command_exists()](#checkcommand_exists) - - [check::is_sudo()](#checkis_sudo) -- [Collection](#collection) - - [collection::each()](#collectioneach) - - [collection::every()](#collectionevery) - - [collection::filter()](#collectionfilter) - - [collection::find()](#collectionfind) - - [collection::invoke()](#collectioninvoke) - - [collection::map()](#collectionmap) - - [collection::reject()](#collectionreject) - - [collection::some()](#collectionsome) -- [Date](#date) - - [date::now()](#datenow) - - [date::epoc()](#dateepoc) - - [date::add_days_from()](#dateadd_days_from) - - [date::add_months_from()](#dateadd_months_from) - - [date::add_years_from()](#dateadd_years_from) - - [date::add_weeks_from()](#dateadd_weeks_from) - - [date::add_hours_from()](#dateadd_hours_from) - - [date::add_minutes_from()](#dateadd_minutes_from) - - [date::add_seconds_from()](#dateadd_seconds_from) - - [date::add_days()](#dateadd_days) - - [date::add_months()](#dateadd_months) - - [date::add_years()](#dateadd_years) - - [date::add_weeks()](#dateadd_weeks) - - [date::add_hours()](#dateadd_hours) - - [date::add_minutes()](#dateadd_minutes) - - [date::add_seconds()](#dateadd_seconds) - - [date::sub_days_from()](#datesub_days_from) - - [date::sub_months_from()](#datesub_months_from) - - [date::sub_years_from()](#datesub_years_from) - - [date::sub_weeks_from()](#datesub_weeks_from) - - [date::sub_hours_from()](#datesub_hours_from) - - [date::sub_minutes_from()](#datesub_minutes_from) - - [date::sub_seconds_from()](#datesub_seconds_from) - - [date::sub_days()](#datesub_days) - - [date::sub_months()](#datesub_months) - - [date::sub_years()](#datesub_years) - - [date::sub_weeks()](#datesub_weeks) - - [date::sub_hours()](#datesub_hours) - - [date::sub_minutes()](#datesub_minutes) - - [date::sub_seconds()](#datesub_seconds) - - [date::format()](#dateformat) -- [Debug](#debug) - - [debug::print_array()](#debugprint_array) - - [debug::print_ansi()](#debugprint_ansi) -- [File](#file) - - [file::make_temp_file()](#filemake_temp_file) - - [file::make_temp_dir()](#filemake_temp_dir) - - [file::name()](#filename) - - [file::basename()](#filebasename) - - [file::extension()](#fileextension) - - [file::dirname()](#filedirname) - - [file::full_path()](#filefull_path) - - [file::mime_type()](#filemime_type) - - [file::contains_text()](#filecontains_text) -- [Format](#format) - - [format::human_readable_seconds()](#formathuman_readable_seconds) - - [format::bytes_to_human()](#formatbytes_to_human) - - [format::strip_ansi()](#formatstrip_ansi) - - [format::text_center()](#formattext_center) - - [format::report()](#formatreport) - - [format::trim_text_to_term()](#formattrim_text_to_term) -- [Interaction](#interaction) - - [interaction::prompt_yes_no()](#interactionprompt_yes_no) - - [interaction::prompt_response()](#interactionprompt_response) -- [Json](#json) - - [json::get_value()](#jsonget_value) -- [Miscellaneous](#miscellaneous) - - [misc::check_internet_connection()](#misccheck_internet_connection) - - [misc::get_pid()](#miscget_pid) - - [misc::get_uid()](#miscget_uid) - - [misc::generate_uuid()](#miscgenerate_uuid) -- [Operating System](#operating-system) - - [os::detect_os()](#osdetect_os) - - [os::detect_linux_distro()](#osdetect_linux_distro) - - [os::detect_linux_version()](#osdetect_linux_version) - - [os::detect_mac_version()](#osdetect_mac_version) -- [String](#string) - - [string::trim()](#stringtrim) - - [string::split()](#stringsplit) - - [string::lstrip()](#stringlstrip) - - [string::rstrip()](#stringrstrip) - - [string::to_lower()](#stringto_lower) - - [string::to_upper()](#stringto_upper) - - [string::contains()](#stringcontains) - - [string::starts_with()](#stringstarts_with) - - [string::ends_with()](#stringends_with) - - [string::regex()](#stringregex) -- [Terminal](#terminal) - - [terminal::is_term()](#terminalis_term) - - [terminal::detect_profile()](#terminaldetect_profile) - - [terminal::clear_line()](#terminalclear_line) -- [Validation](#validation) - - [validation::email()](#validationemail) - - [validation::ipv4()](#validationipv4) - - [validation::ipv6()](#validationipv6) - - [validation::alpha()](#validationalpha) - - [validation::alpha_num()](#validationalpha_num) - - [validation::alpha_dash()](#validationalpha_dash) - - [validation::version_comparison()](#validationversion_comparison) -- [Variable](#variable) - - [variable::is_array()](#variableis_array) - - [variable::is_numeric()](#variableis_numeric) - - [variable::is_int()](#variableis_int) - - [variable::is_float()](#variableis_float) - - [variable::is_bool()](#variableis_bool) - - [variable::is_true()](#variableis_true) - - [variable::is_false()](#variableis_false) - - [variable::is_empty_or_null()](#variableis_empty_or_null) -- [Inspired By](#inspired-by) -- [License](#license) - - -## Installation -The script can be installed and sourced using following methods. - -### Method 1 - Git Submodules -If the library is used inside a git project then git submodules can be used to install the library to the project. -Following command will initialize git submodule and download the library to `./vendor/bash-utility` folder. - -```shell -git submodule init -git submodule add -b master https://github.com/labbots/bash-utility vendor/bash-utility -``` - -To Update submodules to latest code execute the following command. - -```shell -git submodule update --rebase --remote -``` -Once the submodule is added or updated, make sure to commit changes to your repository. - -```shell -git add . -git commit -m 'Added/updated bash-utility library.' -``` -**Note:** When cloning your repository, use `--recurse-submodules` flag to `git clone` command to install the git sub modules. - -### Method 2 - Git Clone -If you don't want to use git submodules, you can use `git clone` to download library and then move the files to desired location manually. - -The below command will clone the repository to `vendor/bash-utility` folder in current working directory. - -```shell -git clone https://github.com/labbots/bash-utility.git ./vendor/bash-utility -``` -### Method 3 - Direct Download -If you do not have git installed, you can download the archive of the latest version of the library. Extract the zip file to appropriate folder by following the below command. - -```shell -wget https://github.com/labbots/bash-utility/archive/master.zip -unzip -q master.zip -d tmp -mkdir -p vendor/bash-utility -mv tmp/bash-utility-master vendor/bash-utility -rm tmp -``` - -## Usage -Bash utility functions can be used by simply sourcing the library script file to your own script. -To access all the functions within the bash-utility library, you could import the main bash file as follows. - -```shell -source "vendor/bash-utility/bash-utility.sh" -``` - -You can also only use the necessary library functions by only importing the required function files. - -```shell -source "vendor/bash-utility/src/array.sh" -``` - - - -## Array - -Functions for array operations and manipulations. - -### array::contains() - -Check if item exists in the given array. - -#### Arguments - -- **$1** (mixed): Item to search (needle). -- **$2** (array): array to be searched (haystack). - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found in the array. -- **2**: Function missing arguments. - -#### Example - -```bash -array=("a" "b" "c") -array::contains "c" ${array[@]} -#Output -0 -``` - -### array::dedupe() - -Remove duplicate items from the array. - -#### Arguments - -- **$1** (array): Array to be deduped. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Deduplicated array. - -#### Example - -```bash -array=("a" "b" "a" "c") -printf "%s" "$(array::dedupe ${array[@]})" -#Output -a -b -c -``` - -### array::is_empty() - -Check if a given array is empty. - -#### Arguments - -- **$1** (array): Array to be checked. - -#### Exit codes - -- **0**: If the given array is empty. -- **2**: If the given array is not empty. - -#### Example - -```bash -array=("a" "b" "c" "d") -array::is_empty "${array[@]}" -``` - -### array::join() - -Join array elements with a string. - -#### Arguments - -- **$1** (string): String to join the array elements (glue). -- **$2** (array): array to be joined with glue string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- String containing a string representation of all the array elements in the same order,with the glue string between each element. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s" "$(array::join "," "${array[@]}")" -#Output -a,b,c,d -printf "%s" "$(array::join "" "${array[@]}")" -#Output -abcd -``` - -### array::reverse() - -Return an array with elements in reverse order. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The reversed array. - -#### Example - -```bash -array=(1 2 3 4 5) -printf "%s" "$(array::reverse "${array[@]}")" -#Output -5 4 3 2 1 -``` - -### array::random_element() - -Returns a random item from the array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Random item out of the array. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s\n" "$(array::random_element "${array[@]}")" -#Output -c -``` - -### array::sort() - -Sort an array from lowest to highest. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -1 -2 -4 5 -a -a c -d -``` - -### array::rsort() - -Sort an array in reverse order (highest to lowest). - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- reverse sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -d -a c -a -4 5 -2 -1 -``` - -### array::bsort() - -Bubble sort an integer array from lowest to highest. -This sort does not work on string array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- bubble sorted array. - -#### Example - -```bash -iarr=(4 5 1 3) -array::bsort "${iarr[@]}" -#Output -1 -3 -4 -5 -``` - -### array::merge() - -Merge two arrays. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of first array. -- **$2** (string): variable name of second array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Merged array. - -#### Example - -```bash -a=("a" "c") -b=("d" "c") -array::merge "a[@]" "b[@]" -#Output -a -c -d -c -``` - -## Check - -Helper functions. - -### check::command_exists() - -Check if the command exists in the system. - -#### Arguments - -- **$1** (string): Command name to be searched. - -#### Exit codes - -- **0**: If the command exists. -- **1**: If the command does not exist. -- **2**: Function missing arguments. - -#### Example - -```bash -check::command_exists "tput" -``` - -### check::is_sudo() - -Check if the script is executed with sudo privilege. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If the script is executed with root privilege. -- **1**: If the script is not executed with root privilege - -#### Example - -```bash -check::is_sudo -``` - -## Collection - -(Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -### collection::each() - -Iterates over elements of collection and invokes iteratee for each element. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output of iteratee function. - -#### Example - -```bash -test_func(){ - printf "print value: %s\n" "$1" - return 0 - } -arr1=("a b" "c d" "a" "d") -printf "%s\n" "${arr1[@]}" | collection::each "test_func" -collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -#output - print value: a b - print value: c d - print value: a - print value: d -``` - -#### Example - -```bash -# If other function from this library is already used to process the array. -# Then following method could be used to pass the array to the function. -out=("$(array::dedupe "${arr1[@]}")") -collection::each "test_func" <<< "${out[@]}" -``` - -### collection::every() - -Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If iteratee function fails. -- **2**: Function missing arguments. - -#### Example - -```bash -arri=("1" "2" "3" "4") -printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -``` - -### collection::filter() - -Iterates over elements of array, returning all elements where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -#output -1 -2 -3 -``` - -### collection::find() - -Iterates over elements of collection, returning the first element where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Output on stdout - -- first array value matching the iteratee function. - -#### Example - -```bash -arr=("1" "2" "3" "a") -check_a(){ - [[ "$1" = "a" ]] -} -printf "%s\n" "${arr[@]}" | collection::find "check_a" -#Output -a -``` - -### collection::invoke() - -Invokes the iteratee with each element passed as argument to the iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output from the iteratee function. - -#### Example - -```bash -opt=("-a" "-l") -printf "%s\n" "${opt[@]}" | collection::invoke "ls" -``` - -### collection::map() - -Creates an array of values by running each element in array through iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output result of iteratee on value. - -#### Example - -```bash -arri=("1" "2" "3") -add_one(){ - i=${1} - i=$(( i + 1 )) - printf "%s\n" "$i" -} -printf "%s\n" "${arri[@]}" | collection::map "add_one" -``` - -### collection::reject() - -The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values not matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -#Ouput -a -``` - -#### See also - -- [collection::filter](#collectionfilter) - -### collection::some() - -Checks if iteratee returns true for any element of the array. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If match successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -arr=("a" "b" "3" "a") -printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -``` - -## Date - -Functions for manipulating dates. - -### date::now() - -Get current time in unix timestamp. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- current timestamp. - -#### Example - -```bash -echo "$(date::now)" -#Output -1591554426 -``` - -### date::epoc() - -convert datetime string to unix timestamp. - -#### Arguments - -- **$1** (string): date time in any format. - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp for specified datetime. - -#### Example - -```bash -echo "$(date::epoc "2020-07-07 18:38")" -#Output -1594143480 -``` - -### date::add_days_from() - -Add number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days_from "1594143480")" -#Output -1594229880 -``` - -### date::add_months_from() - -Add number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months_from "1594143480")" -#Output -1596821880 -``` - -### date::add_years_from() - -Add number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years_from "1594143480")" -#Output -1625679480 -``` - -### date::add_weeks_from() - -Add number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks_from "1594143480")" -#Output -1594748280 -``` - -### date::add_hours_from() - -Add number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours_from "1594143480")" -#Output -1594147080 -``` - -### date::add_minutes_from() - -Add number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes_from "1594143480")" -#Output -1594143540 -``` - -### date::add_seconds_from() - -Add number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds_from "1594143480")" -#Output -1594143481 -``` - -### date::add_days() - -Add number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days "1")" -#Output -1591640826 -``` - -### date::add_months() - -Add number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months "1")" -#Output -1594146426 -``` - -### date::add_years() - -Add number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years "1")" -#Output -1623090426 -``` - -### date::add_weeks() - -Add number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks "1")" -#Output -1592159226 -``` - -### date::add_hours() - -Add number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours "1")" -#Output -1591558026 -``` - -### date::add_minutes() - -Add number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes "1")" -#Output -1591554486 -``` - -### date::add_seconds() - -Add number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds "1")" -#Output -1591554427 -``` - -### date::sub_days_from() - -Subtract number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days_from "1594143480")" -#Output -1594057080 -``` - -### date::sub_months_from() - -Subtract number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months_from "1594143480")" -#Output -1591551480 -``` - -### date::sub_years_from() - -Subtract number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years_from "1594143480")" -#Output -1562521080 -``` - -### date::sub_weeks_from() - -Subtract number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks_from "1594143480")" -#Output -1593538680 -``` - -### date::sub_hours_from() - -Subtract number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours_from "1594143480")" -#Output -1594139880 -``` - -### date::sub_minutes_from() - -Subtract number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes_from "1594143480")" -#Output -1594143420 -``` - -### date::sub_seconds_from() - -Subtract number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds_from "1594143480")" -#Output -1594143479 -``` - -### date::sub_days() - -Subtract number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days "1")" -#Output -1588876026 -``` - -### date::sub_months() - -Subtract number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months "1")" -#Output -1559932026 -``` - -### date::sub_years() - -Subtract number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years "1")" -#Output -1591468026 -``` - -### date::sub_weeks() - -Subtract number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks "1")" -#Output -1590949626 -``` - -### date::sub_hours() - -Subtract number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours "1")" -#Output -1591550826 -``` - -### date::sub_minutes() - -Subtract number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes "1")" -#Output -1591554366 -``` - -### date::sub_seconds() - -Subtract number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds "1")" -#Output -1591554425 -``` - -### date::format() - -Format unix timestamp to human readable format. -If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (string): format control characters based on `date` command (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate time string. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo echo "$(date::format "1594143480")" -#Output -2020-07-07 18:38:00 -``` - -## Debug - -Functions to facilitate debugging scripts. - -### debug::print_array() - -Prints the content of array as key value pair for easier debugging. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of the array. - -#### Output on stdout - -- Formatted key value of array. - -#### Example - -```bash -array=(foo bar baz) -printf "Array\n" -printarr "array" -declare -A assoc_array -assoc_array=([foo]=bar [baz]=foobar) -printf "Assoc Array\n" -printarr "assoc_array" -#Output -Array -0 = foo -1 = bar -2 = baz -Assoc Array -baz = foobar -foo = bar -``` - -### debug::print_ansi() - -Function to print ansi escape sequence as is. -This function helps debug ansi escape sequence in text by displaying the escape codes. - -#### Arguments - -- **$1** (string): input with ansi escape sequence. - -#### Output on stdout - -- Ansi escape sequence printed in output as is. - -#### Example - -```bash -txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -debug::print_ansi "$txt" -#Output -\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -``` - -## File - -Functions for handling files. - -### file::make_temp_file() - -Create temporary file. -Function creates temporary file with random name. The temporary file will be deleted when script finishes. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp file. - -#### Output on stdout - -- file name of temporary file created. - -#### Example - -```bash -echo "$(file::make_temp_file)" -#Output -tmp.vgftzy -``` - -### file::make_temp_dir() - -Create temporary directory. -Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. - -#### Arguments - -- **$1** (string): Temporary directory prefix -- $2 string Flag to auto remove directory on exit trap (true) - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp directory. -- **2**: Missing arguments. - -#### Output on stdout - -- directory name of temporary directory created. - -#### Example - -```bash -echo "$(utility::make_temp_dir)" -#Output -tmp.rtfsxy -``` - -### file::name() - -Get only the filename from string path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- name of the file with extension. - -#### Example - -```bash -echo "$(file::name "/path/to/test.md")" -#Output -test.md -``` - -### file::basename() - -Get the basename of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- basename of the file. - -#### Example - -```bash -echo "$(file::basename "/path/to/test.md")" -#Output -test -``` - -### file::extension() - -Get the extension of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **1**: If no extension is found in the filename. -- **2**: Function missing arguments. - -#### Output on stdout - -- extension of the file. - -#### Example - -```bash -echo "$(file::extension "/path/to/test.md")" -#Output -md -``` - -### file::dirname() - -Get directory name from file path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- directory path. - -#### Example - -```bash -echo "$(file::dirname "/path/to/test.md")" -#Output -/path/to -``` - -### file::full_path() - -Get absolute path of file or directory. - -#### Arguments - -- **$1** (string): relative or absolute path to file/direcotry. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. - -#### Output on stdout - -- Absolute path to file/directory. - -#### Example - -```bash -file::full_path "../path/to/file.md" -#Output -/home/labbots/docs/path/to/file.md -``` - -### file::mime_type() - -Get mime type of provided input. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. -- **3**: If file or mimetype command not found in system. - -#### Output on stdout - -- mime type of file/directory. - -#### Example - -```bash -file::mime_type "../src/file.sh" -#Output -application/x-shellscript -``` - -### file::contains_text() - -Search if a given pattern is found in file. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. -- **$2** (string): search key or regular expression. - -#### Exit codes - -- **0**: If given search parameter is found in file. -- **1**: If search paramter not found in file. -- **2**: Function missing arguments. - -#### Example - -```bash -file::contains_text "./file.sh" "^[ @[:alpha:]]*" -file::contains_text "./file.sh" "@file" -#Output -0 -``` - -## Format - -Functions to format provided input. - -### format::human_readable_seconds() - -Format seconds to human readable format. - -#### Arguments - -- **$1** (int): number of seconds. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo "$(format::human_readable_seconds "356786")" -#Output -4 days 3 hours 6 minute(s) and 26 seconds -``` - -### format::bytes_to_human() - -Format bytes to human readable format. - -#### Arguments - -- **$1** (int): size in bytes. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted file size string. - -#### Example - -```bash -echo "$(format::bytes_to_human "2250")" -#Output -2.19 KB -``` - -### format::strip_ansi() - -Remove Ansi escape sequences from given text. - -#### Arguments - -- **$1** (string): Input text to be ansi stripped. - -#### Exit codes - -- **0**: If successful. - -#### Output on stdout - -- Ansi stripped text. - -#### Example - -```bash -format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -#Output -This is bold red text.This is green text. -``` - -### format::text_center() - -Prints the given text to centre of terminal. - -#### Arguments - -- **$1** (string): Text to be printed. -- **$2** (string): Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::text_center "This text is in centre of the terminal." "-" -``` - -### format::report() - -Format String to print beautiful report. - -#### Arguments - -- **$1** (string): Text to be printed on the left. -- **$2** (string): Text to be printed within the square brackets. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::report "Initialising mission state" "Success" -#Output -Initialising mission state ....................................................................[ Success ] -``` - -### format::trim_text_to_term() - -Trim given text to width of the terminal window. - -#### Arguments - -- **$1** (string): Text of first sentence. -- **$2** (string): Text of second sentence (optional). - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- trimmed text. - -#### Example - -```bash -format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -#Output -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -``` - -## Interaction - -Functions to enable interaction with the user. - -### interaction::prompt_yes_no() - -Prompt yes or no question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer \[yes/no\] (optional). - -#### Exit codes - -- **0**: If user responds with yes. -- **1**: If user responds with no. -- **2**: Function missing arguments. - -#### Example - -```bash -interaction::prompt_yes_no "Are you sure to proceed" "yes" -#Output -Are you sure to proceed (y/n)? [y] -``` - -### interaction::prompt_response() - -Prompt question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer (optional). - -#### Exit codes - -- **0**: If user responds with answer. -- **2**: Function missing arguments. - -#### Output on stdout - -- User entered answer to the question. - -#### Example - -```bash -interaction::prompt_response "Choose directory to install" "/home/path" -#Output -Choose directory to install? [/home/path] -``` - -## Json - -Simple json manipulation. These functions does not completely replace `jq` in any way. - -### json::get_value() - -Extract value from json based on key and position. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): id of the field to fetch. -- **$2** (int): position of value to extract.Defaults to 1.(optional) - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string value of extracted key. - -#### Example - -```bash -json::get_value "id" "1" < json_file -json::get_value "id" <<< "${json_var}" -echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -``` - -## Miscellaneous - -Set of miscellaneous helper functions. - -### misc::check_internet_connection() - -Check if internet connection is available. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script can connect to internet. -- **1**: If script cannot access internet. - -#### Example - -```bash -misc::check_internet_connection -``` - -### misc::get_pid() - -Get list of process ids based on process name. - -#### Arguments - -- **$1** (Name): of the process to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- list of process ids. - -#### Example - -```bash -misc::get_pid "chrome" -#Ouput -25951 -26043 -26528 -26561 -``` - -### misc::get_uid() - -Get user id based on username. - -#### Arguments - -- **$1** (username): to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string uid for the username. - -#### Example - -```bash -misc::get_uid "labbots" -#Ouput -1000 -``` - -### misc::generate_uuid() - -Generate random uuid. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If match successful. - -#### Output on stdout - -- random generated uuid. - -#### Example - -```bash -misc::generate_uuid -#Ouput -65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -``` - -## Operating System - -Functions to detect Operating system and version. - -### os::detect_os() - -Identify the OS the function is run on. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If OS is successfully detected. -- **1**: If unable to detect OS. - -#### Output on stdout - -- Operating system name (linux, mac or windows). - -#### Example - -```bash -os::detect_os -#Output -linux -``` - -### os::detect_linux_distro() - -Identify the distribution flavour of linux. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux distro is successfully detected. -- **1**: If unable to detect OS distro. - -#### Output on stdout - -- Linux OS distribution name (ubuntu, debian, suse, etc.,). - -#### Example - -```bash -os::detect_linux_distro -#Output -ubuntu -``` - -### os::detect_linux_version() - -Identify the Linux version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux version is successfully detected. -- **1**: If unable to detect Linux version. - -#### Output on stdout - -- Linux OS version number (18.04, 20.04, etc.,). - -#### Example - -```bash -os::detect_linux_version -#Output -20.04 -``` - -### os::detect_mac_version() - -Identify the MacOS version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If MacOS version is successfully detected. -- **1**: If unable to detect MacOS version. - -#### Output on stdout - -- MacOS version number (10.15.6, etc.,) - -#### Example - -```bash -os::detect_linux_version -#Output -10.15.7 -``` - -## String - -Functions for string operations and manipulations. - -### string::trim() - -Strip whitespace from the beginning and end of a string. - -#### Arguments - -- **$1** (string): The string to be trimmed. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The trimmed string. - -#### Example - -```bash -echo "$(string::trim " Hello World! ")" -#Output -Hello World! -``` - -### string::split() - -Split a string to array by a delimiter. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The delimiter string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns an array of strings created by splitting the string parameter by the delimiter. - -#### Example - -```bash -array=( $(string::split "a,b,c" ",") ) -printf "%s" "$(string::split "Hello!World" "!")" -#Output -Hello -World -``` - -### string::lstrip() - -Strip characters from the beginning of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::lstrip "Hello World!" "He")" -#Output -llo World! -``` - -### string::rstrip() - -Strip characters from the end of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::rstrip "Hello World!" "d!")" -#Output -Hello Worl -``` - -### string::to_lower() - -Make a string lowercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the lowercased string. - -#### Example - -```bash -echo "$(string::to_lower "HellO")" -#Output -hello -``` - -### string::to_upper() - -Make a string all uppercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the uppercased string. - -#### Example - -```bash -echo "$(string::to_upper "HellO")" -#Output -HELLO -``` - -### string::contains() - -Check whether the search string exists within the input string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::contains "Hello World!" "lo" -``` - -### string::starts_with() - -Check whether the input string starts with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::starts_with "Hello World!" "He" -``` - -### string::ends_with() - -Check whether the input string ends with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::ends_with "Hello World!" "d!" -``` - -### string::regex() - -Check whether the input string matches the given regex. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::regex "HELLO" "^[A-Z]*$" -``` - -## Terminal - -Set of useful terminal functions. - -### terminal::is_term() - -Check if script is run in terminal. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -### terminal::detect_profile() - -Detect profile rc file for zsh and bash of current script running user. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -#### Output on stdout - -- path to the profile file. - -### terminal::clear_line() - -Clear the output in terminal on the specified line number. -This function clears line only on terminal. - -#### Arguments - -- **$1** (Line): number to clear. Defaults to 1. (optional) - -#### Exit codes - -- **0**: If script is run on terminal. - -#### Output on stdout - -- clear line ansi code. - -## Validation - -Functions to perform validation on given data. - -### validation::email() - -Validate whether a given input is a valid email address or not. - -#### Arguments - -- **$1** (string): input email address to validate. - -#### Exit codes - -- **0**: If provided input is an email address. -- **1**: If provided input is not an email address. -- **2**: Function missing arguments. - -#### Example - -```bash -test='test@gmail.com' -validation::email "${test}" -echo $? -#Output -0 -``` - -### validation::ipv4() - -Validate whether a given input is a valid IP V4 address. - -#### Arguments - -- **$1** (string): input IPv4 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv4. -- **1**: If provided input is not a valid IPv4. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 4.2.2.2 - a.b.c.d - 192.168.1.1 - 0.0.0.0 - 255.255.255.255 - 255.255.255.256 - 192.168.0.1 - 192.168.0 - 1234.123.123.123 - 0.192.168.1 - ' -for ip in $ips; do - if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi - printf "%-20s: %s\n" "$ip" "$stat" -done -#Output -4.2.2.2 : good -a.b.c.d : bad -192.168.1.1 : good -0.0.0.0 : good -255.255.255.255 : good -255.255.255.256 : bad -192.168.0.1 : good -192.168.0 : bad -1234.123.123.123 : bad -0.192.168.1 : good -``` - -### validation::ipv6() - -Validate whether a given input is a valid IP V6 address. - -#### Arguments - -- **$1** (string): input IPv6 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv6. -- **1**: If provided input is not a valid IPv6. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 2001:db8:85a3:8d3:1319:8a2e:370:7348 - fe80::1ff:fe23:4567:890a - fe80::1ff:fe23:4567:890a%eth2 - 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar - fezy::1ff:fe23:4567:890a - :: - 2001:db8:: - ' -for ip in $ips; do - if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi - printf "%-50s= %s\n" "$ip" "$stat" -done -#Output -2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -fe80::1ff:fe23:4567:890a = good -fe80::1ff:fe23:4567:890a%eth2 = good -2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -fezy::1ff:fe23:4567:890a = bad -:: = good -2001:db8:: = good -``` - -### validation::alpha() - -Validate if given variable is entirely alphabetic characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is only alpha characters. -- **1**: If input contains any non alpha characters. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abcABC' -validation::alpha "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_num() - -Check if given variable contains only alpha-numeric characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is an alpha-numeric. -- **1**: If input is not an alpha-numeric. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc123' -validation::alpha_num "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_dash() - -Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is valid. -- **1**: If input the input is not valid. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc-ABC_cD' -validation::alpha_dash "${test}" -echo $? -#Output -0 -``` - -### validation::version_comparison() - -Compares version numbers and provides return based on whether the value in equal, less than or greater. - -#### Arguments - -- **$1** (string): Version number to check (eg: 1.0.1) - -#### Exit codes - -- **0**: version number is equal. -- **1**: $1 version number is greater than $2. -- **2**: $1 version number is less than $2. -- **3**: Function is missing required arguments. -- **4**: Provided input argument is in invalid format. - -#### Example - -```bash -test='abc-ABC_cD' -validation::version_comparison "12.0.1" "12.0.1" -echo $? -#Output -0 -``` - -## Variable - -Functions for handling variables. - -### variable::is_array() - -Check if given variable is array. -Pass the variable name instead of value of the variable. - -#### Arguments - -- **$1** (string): name of the variable to check. - -#### Exit codes - -- **0**: If input is array. -- **1**: If input is not an array. - -#### Example - -```bash -arr=("a" "b" "c") -variable::is_array "arr" -#Output -0 -``` - -### variable::is_numeric() - -Check if given variable is a number. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is number. -- **1**: If input is not a number. - -#### Example - -```bash -variable::is_numeric "1234" -#Output -0 -``` - -### variable::is_int() - -Check if given variable is an integer. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is an integer. -- **1**: If input is not an integer. - -#### Example - -```bash -variable::is_int "+1234" -#Output -0 -``` - -### variable::is_float() - -Check if given variable is a float. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a float. -- **1**: If input is not a float. - -#### Example - -```bash -variable::is_float "+1234.0" -#Output -0 -``` - -### variable::is_bool() - -Check if given variable is a boolean. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a boolean. -- **1**: If input is not a boolean. - -#### Example - -```bash -variable::is_bool "true" -#Output -0 -``` - -### variable::is_true() - -Check if given variable is a true. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is true. -- **1**: If input is not true. - -#### Example - -```bash -variable::is_true "true" -#Output -0 -``` - -### variable::is_false() - -Check if given variable is false. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is false. -- **1**: If input is not false. - -#### Example - -```bash -variable::is_false "false" -#Output -0 -``` - -### variable::is_empty_or_null() - -Check if given variable is empty or null. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is empty or null. -- **1**: If input is not empty or null. - -#### Example - -```bash -test='' -variable::is_empty_or_null $test -#Output -0 -``` - - - -## Inspired By - -- [Bash Bible](https://github.com/dylanaraps/pure-bash-bible) - A collection of pure bash alternatives to external processes. - -## License - -[MIT](https://github.com/labbots/google-drive-upload/blob/master/LICENSE) diff --git a/src/configng/functions/bash-utility-master/bash_utility.sh b/src/configng/functions/bash-utility-master/bash_utility.sh deleted file mode 100644 index 65411add0..000000000 --- a/src/configng/functions/bash-utility-master/bash_utility.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck disable=SC1091 -source src/array.sh -source src/string.sh -source src/variable.sh -source src/file.sh -source src/misc.sh -source src/date.sh -source src/interaction.sh -source src/check.sh -source src/format.sh -source src/collection.sh -source src/json.sh -source src/terminal.sh -source src/validation.sh -source src/debug.sh -source src/os.sh - - diff --git a/src/configng/functions/bash-utility-master/bin/bashdoc.awk b/src/configng/functions/bash-utility-master/bin/bashdoc.awk deleted file mode 100644 index 13efdaf7b..000000000 --- a/src/configng/functions/bash-utility-master/bin/bashdoc.awk +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/awk -f - -# Varibles -# style = readme or doc -# toc = true or false -BEGIN { - if (! style) { - style = "doc" - } - - if (! toc) { - toc = 0 - } - - styles["empty", "from"] = ".*" - styles["empty", "to"] = "" - - styles["h1", "from"] = ".*" - styles["h1", "to"] = "# &" - - styles["h2", "from"] = ".*" - styles["h2", "to"] = "## &" - - styles["h3", "from"] = ".*" - styles["h3", "to"] = "### &" - - styles["h4", "from"] = ".*" - styles["h4", "to"] = "#### &" - - styles["h5", "from"] = ".*" - styles["h5", "to"] = "##### &" - - styles["code", "from"] = ".*" - styles["code", "to"] = "```&" - - styles["/code", "to"] = "```" - - styles["argN", "from"] = "^(\\$[0-9]) (\\S+)" - styles["argN", "to"] = "**\\1** (\\2):" - - styles["arg@", "from"] = "^\\$@ (\\S+)" - styles["arg@", "to"] = "**...** (\\1):" - - styles["li", "from"] = ".*" - styles["li", "to"] = "- &" - - styles["i", "from"] = ".*" - styles["i", "to"] = "*&*" - - styles["anchor", "from"] = ".*" - styles["anchor", "to"] = "[&](#&)" - - styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" - styles["exitcode", "to"] = "**\\1**: \\2" - - styles["h_rule", "to"] = "---" - - styles["comment", "from"] = ".*" - styles["comment", "to"] = "" - - - output_format["readme", "h1"] = "h2" - output_format["readme", "h2"] = "h3" - output_format["readme", "h3"] = "h4" - output_format["readme", "h4"] = "h5" - - output_format["bashdoc", "h1"] = "h1" - output_format["bashdoc", "h2"] = "h2" - output_format["bashdoc", "h3"] = "h3" - output_format["bashdoc", "h4"] = "h4" - - output_format["webdoc", "h1"] = "empty" - output_format["webdoc", "h2"] = "h3" - output_format["webdoc", "h3"] = "h4" - output_format["webdoc", "h4"] = "h5" - -} - -function render(type, text) { - if((style,type) in output_format){ - type = output_format[style,type] - } - return gensub( \ - styles[type, "from"], - styles[type, "to"], - "g", - text \ - ) -} - -function render_list(item, anchor) { - return "- [" item "](#" anchor ")" -} - -function generate_anchor(text) { - # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 - text = tolower(text) - gsub(/[^[:alnum:]_ -]/, "", text) - gsub(/ /, "-", text) - return text -} - -function reset() { - has_example = 0 - has_args = 0 - has_exitcode = 0 - has_stdout = 0 - - content_desc = "" - content_example = "" - content_args = "" - content_exitcode = "" - content_seealso = "" - content_stdout = "" -} - -/^[[:space:]]*# @internal/ { - is_internal = 1 -} - -/^[[:space:]]*# @file/ { - sub(/^[[:space:]]*# @file /, "") - - filedoc = render("h1", $0) "\n" - if(style == "webdoc"){ - filedoc = filedoc render("comment", "file=" $0) "\n" - } - -} - -/^[[:space:]]*# @brief/ { - sub(/^[[:space:]]*# @brief /, "") - if(style == "webdoc"){ - filedoc = filedoc render("comment", "brief=" $0) "\n" - } - filedoc = filedoc "\n" $0 -} - -/^[[:space:]]*# @description/ { - in_description = 1 - in_example = 0 - - reset() - - docblock = "" -} - -in_description { - if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) { - if (!match(content_desc, /\n$/)) { - content_desc = content_desc "\n" - } - in_description = 0 - } else { - sub(/^[[:space:]]*# @description /, "") - sub(/^[[:space:]]*# /, "") - sub(/^[[:space:]]*#$/, "") - - content_desc = content_desc "\n" $0 - } -} - -in_example { - - if (! /^[[:space:]]*#[ ]{3}/) { - - in_example = 0 - - content_example = content_example "\n" render("/code") "\n" - } else { - sub(/^[[:space:]]*#[ ]{3}/, "") - - content_example = content_example "\n" $0 - } -} - -/^[[:space:]]*# @example/ { - in_example = 1 - content_example = content_example "\n" render("h3", "Example") - content_example = content_example "\n\n" render("code", "bash") -} - -/^[[:space:]]*# @arg/ { - if (!has_args) { - has_args = 1 - - content_args = content_args "\n" render("h3", "Arguments") "\n\n" - } - - sub(/^[[:space:]]*# @arg /, "") - - $0 = render("argN", $0) - $0 = render("arg@", $0) - - content_args = content_args render("li", $0) "\n" -} - -/^[[:space:]]*# @noargs/ { - content_args = content_args "\n" render("i", "Function has no arguments.") "\n" -} - -/^[[:space:]]*# @exitcode/ { - if (!has_exitcode) { - has_exitcode = 1 - - content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n" - } - - sub(/^[[:space:]]*# @exitcode /, "") - - $0 = render("exitcode", $0) - - content_exitcode = content_exitcode render("li", $0) "\n" -} - -/^[[:space:]]*# @see/ { - sub(/[[:space:]]*# @see /, "") - anchor = generate_anchor($0) - $0 = render_list($0, anchor) - - content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n" -} - -/^[[:space:]]*# @stdout/ { - has_stdout = 1 - - sub(/^[[:space:]]*# @stdout /, "") - - content_stdout = content_stdout "\n" render("h3", "Output on stdout") - content_stdout = content_stdout "\n\n" render("li", $0) "\n" -} - -{ - docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso - if(style == "webdoc"){ - docblock = docblock "\n" render("h_rule") "\n" - } -} - -/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example { - if (is_internal) { - is_internal = 0 - } else { - func_name = gensub(\ - /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \ - "\\3()", \ - "g" \ - ) - doc = doc "\n" render("h2", func_name) "\n" docblock - if (toc) { - url = generate_anchor(func_name) - - content_idx = content_idx "\n" "- [" func_name "](#" url ")" - } - } - - docblock = "" - reset() -} - -END { - if (filedoc != "") { - print filedoc - } - - if (toc) { - print "" - print render("h2", "Table of Contents") - print content_idx - print "" - print render("h_rule") - } - - print doc -} diff --git a/src/configng/functions/bash-utility-master/bin/generate_readme.sh b/src/configng/functions/bash-utility-master/bin/generate_readme.sh deleted file mode 100644 index 858a4b8be..000000000 --- a/src/configng/functions/bash-utility-master/bin/generate_readme.sh +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env bash - -#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh -#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh -_usage() { - printf " -Script to autogenerate markdown based on bash source code.\n -The script generates table of contents and bashdoc and update the given markdown file.\n -Usage:\n %s [options.. ]\n -Options:\n - -f | --file - Relative or absolute path to the README.md file. - -s | --sh-dir - path to the bash script source folder to generate shdocs.\n - -l | --toc-level - Minimum level of header to print in Table of Contents.\n - -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n - -w | --webdoc - Flag to indicate generation of webdoc.\n - -p | --dest-dir - Path in which wedoc files must be generated.\n - -h | --help - Display usage instructions.\n" "${0##*/}" - exit 0 -} - -_setup_arguments() { - - unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR - MINLEVEL=1 - MAXLEVEL=3 - SCRIPT_FILE="${0##*/}" - declare source="${BASH_SOURCE[0]}" - while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - source="$(readlink "$source")" - [[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located - done - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md" - SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src" - WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions" - - SHORTOPTS="whp:f:m:d:s:-:" - - while getopts "${SHORTOPTS}" OPTION; do - case "${OPTION}" in - -) - _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } - case "${OPTARG}" in - help) - _usage - ;; - file) - _check_longoptions "${!OPTIND}" - SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-level) - _check_longoptions "${!OPTIND}" - MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-depth) - _check_longoptions "${!OPTIND}" - MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - sh-dir) - _check_longoptions "${!OPTIND}" - SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - webdoc) - WEBDOC=true - ;; - dest-dir) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - '') - _usage - ;; - *) - printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - ;; - h) - _usage - ;; - f) - SOURCE_MARKDOWN="${OPTARG}" - ;; - m) - MINLEVEL="${OPTARG}" - ;; - d) - MAXLEVEL="${OPTARG}" - ;; - s) - SOURCE_SCRIPT_DIR="${OPTARG}" - ;; - w) - WEBDOC=true - ;; - p) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - :) - printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - ?) - printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - done - shift "$((OPTIND - 1))" - - if [[ -w "${SOURCE_MARKDOWN}" ]]; then - declare src_file src_extension - src_file="${SOURCE_MARKDOWN##*/}" - src_extension="${src_file##*.}" - if [[ "${src_extension,,}" != "md" ]]; then - printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 - fi - else - printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 - fi - - if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then - printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 - fi - - declare re='^[0-9]+$' - if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then - echo "error: Not a number" >&2 - exit 1 - fi - if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then - printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 - fi - - [ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}" - -} - -_setup_tempfile() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -_generate_shdoc() { - declare file - file="$(realpath "${1}")" - if [[ -s "${file}" ]]; then - awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2" - #awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md" - fi -} - -_insert_shdoc_to_file() { - declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc - shdoc_tmp_file="$1" - source_markdown="$2" - - start_shdoc="" - info_shdoc="" - end_shdoc="" - - sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then - # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file - - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" - echo -e "Updated bashdoc content to ${source_markdown} successfully\n" - - else - { - printf "%s\n" "${start_shdoc}" - cat "${shdoc_tmp_file}" - printf "%s\n" "${end_shdoc}" - } >> "${source_markdown}" - echo -e "Created bashdoc content to ${source_markdown} successfully\n" - fi -} - -_process_sh_files() { - declare shdoc_tmp_file source_script_dir source_markdown - source_markdown="${1}" - source_script_dir="${2}" - shdoc_tmp_file=$(_setup_tempfile) - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_shdoc "${line}" "${shdoc_tmp_file}" - done - _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" - rm "${shdoc_tmp_file}" - -} - -_generate_toc() { - - declare line level title anchor output counter temp_output invalid_chars - - invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" - while IFS='' read -r line || [[ -n "${line}" ]]; do - level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" - title="$(echo "${line}" | sed -E 's/^#+ //')" - [[ "${title}" = "Table of Contents" ]] && continue - - # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text - anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" - - # check new line introduced is not duplicated, if is duplicated, introduce a number at the end - temp_output=$output"$level- [$title](#$anchor)\n" - counter=1 - while true; do - nlines="$(echo -e "${temp_output}" | wc -l)" - duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)" - if [ "${nlines}" = "${duplines}" ]; then - break - fi - temp_output=$output"$level- [$title](#$anchor-$counter)\n" - counter=$((counter + 1)) - done - - output="$temp_output" - - # grep: filter header candidates to be included in toc - # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment - done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")" - - # when in toc we have two `--` quit one - echo "$output" - -} - -_insert_toc_to_file() { - - declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash - source_markdown="${1}" - toc_text="${2}" - start_toc="" - info_toc="" - end_toc="" - - toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" - # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command - utext_ampersand="id8234923000230gzz" - utext_slash="id9992384923423gzz" - toc_block="${toc_block//\&/${utext_ampersand}}" - toc_block="${toc_block//\//${utext_slash}}" - - # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 - # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches - if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then - # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace - sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" - echo -e "Updated TOC content in ${source_markdown} succesfully\n" - - else - sed -i 1i"$toc_block" "${source_markdown}" - echo -e "Created TOC in ${source_markdown} succesfully\n" - - fi - - # undo symbol replacements - sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" - sed -i "s,${utext_slash},\/,g" "${source_markdown}" - -} - -_process_toc() { - declare toc_temp_file source_markdown level toc_text - source_markdown="${1}" - - toc_temp_file=$(_setup_tempfile) - - sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" - - level=$MINLEVEL - while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do - level=$((level + 1)) - done - - MINLEVEL=${level} - toc_text=$(_generate_toc "${toc_temp_file}") - rm "${toc_temp_file}" - - _insert_toc_to_file "${source_markdown}" "${toc_text}" -} - -_generate_webdoc() { - declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file - declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc - declare webdoc_lastmod_date webdoc_lastmod_epoc - file="$(realpath "${1}")" - dest_dir="${2}" - - filename="${file##*/}" - file_basename="${filename%.*}" - dest_file_path="${dest_dir}/${file_basename}.md" - file_modified_date="$(date -r "${file}" +"%FT%T%:z")" - file_modified_date_epoc="$(date -r "${file}" +"%s")" - - start_shdoc="" - end_shdoc="" - if [[ ! -f "${dest_file_path}" ]]; then - - cat << EOF > "${dest_file_path}" ---- -title : -description : -date : ${file_modified_date} -lastmod : ${file_modified_date} ---- -${start_shdoc} -${end_shdoc} -EOF - is_new_file=true - else - is_new_file=false - webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")" - webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")" - fi - - if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then - - shdoc_tmp_file=$(_setup_tempfile) - if [[ -s "${file}" ]]; then - awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}" - fi - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}" - fi - - # Extract title and description from webdoc - title="$(sed -ne 's/-->//; s/^.*//; s/^.*/${title}/g" "${dest_file_path}" - sed -i -e "s//${description}/g" "${dest_file_path}" - else - sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}" - sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}" - - fi - - # Update the last modified timestamp in front matter - sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}" - - echo -e "Updated bashdoc content to ${dest_file_path} successfully." - rm "${shdoc_tmp_file}" - - fi -} -_process_webdoc_files() { - declare source_script_dir dest_dir - - source_script_dir="${1}" - dest_dir="${2}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_webdoc "${line}" "${dest_dir}" - done -} - -_count_library_functions() { - declare source_script_dir - source_script_dir="${1}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - { - declare -i function_count=0 count=0 - while IFS= read -r -d '' line; do - count=0 - count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}") - function_count=$((function_count + count)) - done - printf "Total library functions: %s \n" "${function_count}" - - } -} -main() { - # export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' - # set -x - trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT - set -o errexit -o noclobber -o pipefail - - _setup_arguments "${@}" - _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" - _process_toc "${SOURCE_MARKDOWN}" - - if [[ -n ${WEBDOC} ]]; then - _process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}" - fi - _count_library_functions "${SOURCE_SCRIPT_DIR}" -} - -main "${@}" diff --git a/src/configng/functions/bash-utility-master/image/bash-utility.png b/src/configng/functions/bash-utility-master/image/bash-utility.png deleted file mode 100644 index c1bfb8b556809ce645cf41ab6d4b11547df68fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32396 zcmXtf1yCFB^LKD}cXxMpmqH2@E8aqbYjJmq6{k4GOR?ha?(XjHZts16@BD8j$xLQ) zxqI%}-H+@>fYlVxkcp8&AP}08qO1n+`Sjll5gvFCKhYNhK9C#~^_)Q<^zQ#&Q2qAB zUx6=)T;z0JH0>>1+)SO!L2hnt?3Q-c&RF#l+*mh9L{KLe?Hj$BzM_YvZ_Z&jCIBMaRuo{e} zM5zbeL%0kbWou*e<;!E&eMp=QS)I*lJ@3>AB<8IAJoZKK*cDiq@6JPm^hc1R^ORp>R*A83$2T>98`P20;Pf9b0u}uBp8>~TJIO?)!m?=s#71!w5mW{athncpKrg51uS`NAq`wJi z4CD%!$!B3=xD@r6a#b}-poG%AJMVYv9E*j6x9JK#!knXhdVS^9xGjQmiA&~?V@OHK z;9U&bHG%cv)Hf{mqwDu;&Qf)+8l$V<4#Qy;N@LC-!7C@PDsIOe1<(3%N|Tm)%b;sh zi!I1vsHOxd*4&wl5;Ga))_kdfi83%U^Vm&h(jIf5T5`i;;}Ci@Gu%^T3BqDBR?br} zt5f<8DKQCIv2Nt;Je*7ut>VxA&F0LA`4A%w=OAAuPb{qn*9!80@vX6x(c1%sBY8H& zP!>+^q&jK*6e%8 zD8+geQ80K|^o7{TI7=(bA&^{=CPWvdNq4`k+?W~!R{jP2yg;V`i3%jSKbl@-P;Zw> z&%mHy$^8pgyEMsDl>F>uvHHyT{b^nd1gmUmPlAt!#}yJ1l88yIh)ZWtGH7Yc#D+iE zd^xR5ykQ;o1PLNiX@hP_`#&|Vp8Xpd8XC55z16-Zv2WRg0x2_n*3t_8emr;C zX>PUy>r?sIbA?{$H-v<*Fw8RL{V@O*Px0$Z4}@lrqIBIP%Y&Eds}C{?%Hr$u{j(v* z-u1Xd_R7M-L*n!yD>iIysK(GoWsh}r#5bfs%7-(@K*|L%@^O#m)wiSF;gs{|Gq-k< zbB_I)5>4eBV|%Ll%$?L}rz^R5V5e<9Svlx4ifeoi57l85Le?pS(B)6bMB@g~q$VD+ zy9EQdV}_{iQ$VR839I4q|C}3F>J2eheF4Uy&hBsdKE5 zz|lbNREEG*t)naxw$0RE6GO*Q<^B8N4{}pR3@0gl3GS-@m}!-+qLLC)<93V)VTg3D zUYqwLYO9f7`=X6enMMgc7i!)GQjH{2$n-8XdUMdiiK|)l@`g*(#-)1)Cu$-xT#Yc9 zs-yg$S*zUX!(!!ho5$r61_0U#)6| zv(5F&j6aKHs!h$TL=CZg+L1n#Ghl2*w04GRYPNL{&h!Z^*OD%p?!x^hukVR(G2V#e zq3O&;B@#(bOAAgmf9(`?7tKOX8U8-8>ENyV>C>l|tw;`j@#mB3n&M(aIXO8EVk6Wl zQcQ%rs&1sF8J~fi1ZD|BCj<&6S)0oqu>_AD0 z3CYH$4~BxG1b5I>f{O`hAxWuXZsKS99t`*+*|8&+M9@^QIk>nRa8ku?!kGvn!ovJU zO)iH6aWo1||1A+nDZ!VsODJP5IWZJ1KYE4?$$U4o+FxdtWhMvr-v{ZkC=_FMd#T!g5xRjeXOn7`?R{kH|3 z9*6L*&iY1o)_pHgt|rG`H(=5A%g~YGP`pym%FsR>)YWy*najE9)ZhD+;;kBL3zD)! zROwT1^c@2It9NxS24i9`%@64yyjJn?8K`AyDj-bhaHSHhy+fC#f{2KSZ8Uh9gi#A? zeIS)o`n~(|GcX|Z_V#{p{Fy|I+5^k&?*DVTqBmHRH5<1AJfzK<%?Q8T*b7ZTV|y3d zJGO@)^!+GtYdcnA57Cc#70zT(#-3ajvFq)KXr7kYiDPtf7a?&RvM88F;Eh!u(>onL)3@Z&*NUq zX7v$C15KE=sIdFgF|Yg&_R$Wj_j|f=_wNuCP7gD5Y;Wi>?pHTjFHuq(R}x}k;*Xw( z)Z&ZH6wmj*$WW;9@z|Y}tn3*q3K?pV`@T20XU46_jR_2zNy>6Wp}C0~@SvuqCUjmt zzSgz5xjKacjqdOiPbQGy{-*8b$&S`3GZ z^5vfKdiUiQgic1hiN0S{?kf;W8_Bo7LhkOPtqr!lwgY2B`OHbmCWN6-l49iQVXJoQ zo0snVvvYG??$T=9cUz|O!*TYIidNl7WTc=o6OwVU=4#+JW1Q%l>3 zUQR^3Ggqt`>{x)T?~HxJj6Y{bq7jedcz;^I`NmfoCt#~D0acU;9jw^SdxSCO!2~NM zY5kcbhD;*h*6*2Q?2{3keLXch7164pp$Q!N^mU=>=~At(?=g4O_&^TpFfs_SAaUQL zZD`Zsq`0DDUHkKAue)f0wMsmEeCe;scDzz*4r(#|pp?qcD zCr5JR;AGR92nc^6%fQdDuW81DF{#o`-P!pXiYfhBUw`YY>9{=bifVD4Q0i!V*zHMz@-B9orM)21xE7w$wHM!Sy@>{lR>$1 z*1nt9(nBqZ1F8&VSHp;SQ#*HNRS(|=Y?2Q*L3gH|| zyiBXy^bBfBtEDw~DsjzRCu|7Ux>UV6FCbE_yfDO2M zg>4JV67q^^yBL?q(-TU7IPJ+cUCb>Xv=j_8rf10{Xlfi(_&tkwmNn^Pwy<0aN@2RM zkIzm{ULHPP@1Mix1nX?P!WiXKO1&$jsk7R%BE&#EB6gFy{U%<;al0L$@h#zKur_jW zZy?vod)2zEs0CU`{v{O^>Q)d4W6h$Cmr%@cZX$9E278JMBTGe2`%}gG#L6#cOEZ1p z%+?)z;qyInUOKTLdb$re?@#CNQo}>T&z6Rb&}yyCy6^?7a8fyjhK4TdZ?89P6@EeV z`T3JAdd`P0<}8r67C0@EZ-pvnMjr%##8OsTQA8fwFW~PXFxjwrb~ZLPHg9cZg+Dtp z17X9Dr0(cKq6?R~vI~Sr->!oLnfkspxJT?J$TCX5w<^+{)2c3kzx4P>M)r37m(7G6 zHb7^M`{X$Nj@K5WX}4~~(R<{{<_X0d$a^l0iXUv*78|cMPhP$vsAY=7T{ozv)>ti& zqE~8loWjAm-^6`S75K*Q-R4jsA(nBGqQoTkMHA--25A?C(X(-4tqYZ0slR@af`S)G zxMY>hBDd&G2Qh1^1O?Ne5_C6bB4*cvLBddZdHKDSm6bR<2SbnsETQ_(Ip+S1@A-VgRaCK)RqCyXUcWaMaeoQgPh$s=Cn1_Awmml&adap&CF?MK$?VaiMYV^#nc45L z?MoNn(7nFCw)FKCPsq+zhlX}_QeEk&F&QQL0Z+}VaTt_KqHL7=Fv!k3!^o8e=d-}q z)N5v{tgNgE{GwKu^#{+lv7LgVq6)rWs`n*Ye}K}X6^w}(=zkZ3?sW5jbmjXIE5`35 zba_9zvoy8--_?!3r>Q*O#LpV7>9p6ri*WbHo)aj=YM1pA`O$AVi$@W2U&0^T?$@l3Ix6%sko#in2Be9qbiKN!_civ z*42WZm-vG{kp{6ScvzXr;fXcg+iB<;MYI4{+nQj^_yL;2ZZ*cR*|qb`iH464tz|fb zxD-tf9xuz|PVaBqJ$YJO98_O30r`bps%7<}4MLgradvfeHO5EfRRA!LaE`Wz@uYYE zn#?_*=3C9%#T!w_r$#c7y^gH0X)IJ{J)(d9R;7#EHmy?$03urtk4GOdF|lmKzzH%3 zClqxiJS!{f!*kzvgftOn{twC|3j3^WA?yw2LVOIg8uWCLZ#UoYJ#+IDn^x_Z5k>J} z+XNQ)aj|Z774}}g@3?2x)w)f@y7@|<`y62XiRGDIA#y&5$8(!Fbl%gfrHWy1vF3Yq z-)E?qwz|!B>3;l14&>cR{j!QIK}Twe3-Sa!+{)(5NR7IDSQR_4>{t$E=q6!q81#%; z73bmM;R(;UX!rSgyYcMWn)vJ8vb~>j#k|B{Fe7%!d|aVuKUZ0q?br7J824}8@}v(o z6x{T?)LdiXPj~1pb-ZS`hAfvm^=%;qD#?;%+8!u`0g|6tF zH4d|^cIsqKgh2j=ZMbd+AW-z=VMqk)@qFuma3X>jw+&@EfOK^=H#a9?43M)tl@#{b zy}i9D7zFeVvnmI|rF)S%tV?X&#&G&~aQd9g6c$kFF3;Qff2k7ZO$u_cupGbj^z#nV>B_Spu!5|ZMDW(z=%RF0c)?s93CN3McR3PTUyXg>}WPPA;4m=~c z>g$v51!yq+flE{KjpwU=nRaukDIpz*8WH;R?2LZDtwFNRc0HywEcSO<8@}as^@OFH zzi`vfxTNUi^EL#Gb!P(l4{3?~`w6yQikg~4D*2So$Zf{R+ zJ9JuoH<^oK(yg^X(_XEOkDVUEjbD2m~nsf0w@ss z{P2fEz6E`w2f}}MT;mgeaFMYi}R-`gNLaS=h!R!A=W}F+!z$I z!d-ABmp@5^bpzo{;}eE!VQ8UcU`W7Y(Y~l?y-RD$$=OH-uwOs0XPlDFH8;fGQbCln zvwUBl%f;M}SJ5CPrJ-vkp~`vkKHu?*xyWPJsfV!=%BwRiNa z6&t{Er@ZP{oK%Pkv>4ghEwumYVhuiipJ$==d{gu^8OaOclAQUd0@UIM>O)x2VuRxs zBWLI9cfrgtVku0~w(2P%<&p`@TI6LQH|R)cDQ;t9AMPw(AO7BDfykes{+UQUc*Ve* zDid)9m)4f9H{Ha=B*rE)wl1F*CZ%vpufLsJ^9uQ~e=44F85S&bfUlcSNREbgla7)2y3-&B9OQn`L#*skrmF7bWVnB*%cL3KEJ{ zv#LsvqNk@Yzzj9kyst;?$Fe2f+g0hOnUl@8Us_IG5xib@n68@F-veHzW8a}>`56lL z)u0MGW=nOrxMx|NiZ+n2b6 z(S7!i+9eThhGKsWWiL-pMGrZ;NB4cTm)%y32|b<2L661@!PUy;X3THA8*O|T8@YU? zoVHeW75*5-22tL7kk{SLy=Xe0dRCpTUG-AnG&y_X^Il1S7DFD`D>FH@D4__}}VeNa$XAWKLzBd|t zZhJkoPqAID8{I{j>!mgkz~_WWVW^_KiC84XNvgsDTLsfS$g(R#$oVD#NNCw{ahExr zCc1;1s_5OEf6tG9=QkB5pNP04lB~aD&JS^Pb~BoA5>70xAQp56_f}t3oy93sLOQ)^ z={zyCTBw$SUU>PbH7RG7`5tuEAkqhL=->NNQgq){Lb+-J>^Agi7ziShH(JeS*@ zjTL`-@j0R=SeZ<8+;4Ja<;C>Nm+hEuSsr^=C>$-6|I-)C?;r0|EnOsD9gscqG0PI( zG5h|@8*P@=CBw;VUNc|0%RZLQCLmD}gMu-kt#t^5T(@97_E)0?J$E?6UG1_!~+Nm$kwlV;+v@()GZr059jNjfI;<*V+_!~8B<@#Y+PBQ;Azf2x59_qSIDo`lxXEmf~7DSyE$3Mugic)w9(X@?gMJ%Y0G)rbK1*v zK^umeL2S5X7;?;2FDtho>@5u_{5%Nptt0Ppsn%M3`SeaRkWvu#oQaXNAknQ^tZUJE zKP0>5W_;!=zvgtlXwyaVF0Xwj_8%3sJZA#-!uHqodC2ZA%gmt@T6q35*VN?c@#en0 zPS(Dqo11```wrvl=CwpFa={PcDJLLvupC5%s6ehBY?>g4+)qXAFBAY1A%H!P-0;ZS zZ6et1(k#23^l-f2)5Xt>+ zuvD}8(U@mDz8}uZIM~#5+tqJh>?>S`3A{zYWc!3B{w1YNiz>FWRk*HY(e=4seHe3V z3&N{kgN~M$AqvHU?et>i>vy~bxE2Y!QOkqh?d|O+;8HTiyArNL7ODcdPV*Fyq5W}j zaXI1O;B=c@N_@M5k*YN{H0Fpc5`Di6EM1RZY>vEwy! zW=STdS3}Cj&`0zgcziEL>MPwhrBhYc^y)fbDD4mrCTQHdRX^C0xt=UVx3|_~-OlRd zMn;C|=gF_mvK(k1T&#Wv;P&hNm%AvU83-?UeFq5&6x;7yn3wm($Ib2jPRH1%))mlB zUMSu!5Ii$skIjzqFkJ7Kek#kGnoqt@a4@lFsIj<+o z)LKFdmd}CPb!l|C)F7&}Sq#i72+Ao4;rd0`7J{|V#yU*RM+fI@G2S`y>yc8cW3~ytMqtj=KpS1ymOn8= zBr=adJ-1+3USn&t2bTaLL>dZ&T%aK&#LjNlHuzPULc-U(+j(4Uz6AjKJNx^24Gr%3 zFuRj%+*L#$j}|5j`=Mu&=fYS^8EJh z1n9pdGvwg}OeYt>v4ce04-HTcc?3}o_R!3RMF-sgp`LBCuJKmmrLK+Vkrb!GG` zan`nAjhoBHLkZ8q<)y2Q`@JE=MQI>Q5S_o7jJ=-3BY|OaQh~gatiDXrN}1bSFBut- zQw0s#=c`I}D&EEc=cnCm^0!>eIgiGdTe+C0RqM`qjawpz*PCf|mNoZnG;;qx)z#NY zK-7JMIsk#C5vKtYf~?s!?)_nx(TEV;_kiNCM)er)btxa&oe zM~a}RVXFYP3+S}H)9MmsfWq;_v0jJ}uoloZq&}_C_GoLx%P{dDR~ltG%l81d zQuKdZ=^NTB{u9TD z2`JQ+q~Tvr&yDlg*vb+KV5Kh9Dv;HRs&{Sz-OQ*-{8%@7v~1m%6>!(B8ZQ*#u4 zq?VKeDuwIZU%ipC`n5lPCsqA#RCIK^a_fvAY+Xg358K8ss#DkmU7uM0F{lLwQZ*Q? zqb9^}y|=z_8$EU2SDLIDKx)%`;|v?y>PXwg;#-{DBYWX?F5)#S6o;Y`>2VEB(lWer zAumg-xZip*^mwYiZc1PL#oljk0Hu_X0GdTO&p}lIgl0)Zgp0}xT1ion5|v6T7fjpV z+xx+w(P?*Avq)_gh_RnrTU%A+IWU~u>k(7OPF)0sX4gtoMfkz{uY}=7I^K!0D@F zXDf{;@7EITaxy9?8k*A+vx`t{Di_i1Ua;>kQ|gXholdlL^moUkaq!c2uP+-R6^cFN17~!g+-P=x)u@`mQvR~mBeOJzY5M{xu-^@wlhHUgul%3;ns;@dQcBNq4IuckDVt|H!k~ci- zQWZXj5jKle>K(L+XZsm$>hQbpj_N=k5Z^iAKMqy@u-QgWfeV(D^B@9r%s3IglzdvR zAQ$E&s}|{86W^!lZ1QwoOAU1=C+y4(>$QUFkg=h+Bm{M8qgX1R7OSuAlxw)k%EOv&W@At9e+k0}6fI*{>Nl$Kj+eqHY ziMlmvG4jg;=N}KZ!&_HMag%ne<#!1Ls^dY^jsHLSPFDFNmEBW<%V|FAL7W*l z&kR_W>4^!+a)ZYBq_i~8cIn&!>R?PMOz=%-*iPQ>dxz;LrOLWmYCkLVx7{L%&Kafz zD0hWh@m%7U4N{hB_lBamzN-%I)TZZEwgQcaWt8M?9f@bArBV_pIOMIP&O0`Wjg?0Z z#PMrZs^BA+rbf3F$9@{XbwizK?)6ZmPP5mFrA-BLM`1&QZm;9YC~}AyeG5Vd=YLYu z`|A=#T$~6Qpx;bb&bpr=Nc7aK7}(2a{~Q*w{bUEL$C|xB>cRVBZvp}O6VBR>)mkpL zFeWvOQ8JmxQz~Z6rVBA9rlynuw+sX@2!-~$+KLbC?CkhJARsiyyYz7CO2vN1K-=a2 z`QH_kl|{F_Tn#HBAt4RpBFhO=1uvYsdcO?XX&IMEE4L73YO~CVHsAxL47T8$Cd6NK z^l%*oFf(G*ShIDFn3^_w6jLM!Q47y}JdlbhJ96f?JD<<{y9(-a8cT>m1=++Uyx_mJ zvqJS>qk;hYx`@rVttF?SA$7IcjiYlxGX}JyBdiMwW9Fpls}^7r68g3H&m2XDDKmf8 z>Kiv|9>{-K=g-AtG6t9L7OsoS^3Pvcp$KQq1ece4Bw^gXA)6FhukCGYy`W1ycO3e! z+ifEDVUsy9d2_ji<@jN-qyc5k>43`?YKjZoAWN3_)nL!m)D)L>o5E3^!f{GJ^jF~j zcGkSkKhe?Bs_HN&ErK?j8V4Ua*nj>RLe6LC%L72f+2>m1M$JOm+6LO+$Y_Cnl&iXP zm3ert1isu$szK2N+Tm98EZ2$ zE>C;u_T&k1Zd_6rMYS++UYxC;{*spPiUPG+)!dv;uhK-SrPE}c7ztnsz38v(I^~v^lT-NC;E_nI1hU}m zDh`kIeJ#Tm9S_2Rb!b6=g$0lh&b5WfdM6FU&K4@q%!K!70zRj%9GyQs8i251>F_BN z&ad9j%j^8)f({+z<;>jEJ3G;O{Xba_s%|FpwkB6sKd5Dkj{-#AJxSRdT2d%R#@d?2 zv3ZrUjf>%1SMZu*^GssOUuAUZ;i1Vp0Z#XuIwqprd96YblL3KEh_5)_)BTbd6BF66X#so9LlY$vQRPQxZ7-!#8q~kfZbZDcFGDRV7H- zCu>3V>%Jtje6^gZI5}TGjifQIF1V222efyoWl%(HivH_?ycOjLGU2y*o8F}wL8EYa@> zgMhF6+$x9NyMUvDMo>zbwF1XyrX7^wDc{A?VC3{SXQL&k79QvhGGJzrnS7?$D7&xk zn4bIr&~E7*T=b&h&j>=1Nxx4zXP(8xLH?a4gwAh^sh5*<5u^z^`mSzA%wkX|Dr$|= zm}7K|j+j>XfC9zm3k*Id_-n%FgUu_YB&IOG{Bdprk1uc~!Z(kb&}nJuRTaP{_pN~| zQp*ldD3F=kF}Mf>)%TNNeUtrtXl&Xuqkz!^J~1Nc@j@}o+`OAa(6kUll?N;65A+^k zHrUjbHj{iC%k1vE=ii#ROmJv62v<>rHYIcye0N!t3>JA(tZ|FlBlJYYIvr|o;31ea z!)%>@R_7IeLW?2|^m+ex<xmPO?8nLH;Mco;H@(kNzkHrw?^sM=_{qaYp)4nw!7hLp z;nbw6O&1z?cpwAp$BkpfG^RVTboiGpT*~>DWWyWEkt@hv2TioQ%A_p!L$C@?IvJwC z7fiyO>g7{G$0yXa$Hi3sFZ>4N7{FNxjd{f=Ne?RIIOlU*{B)CV*65^o zae0G<{Cv>5oM6Q=)=3#poY?*>1ctu}{bZHbcVVolnh|$x>{lCHM4(W_LCV(6hpSIn z-IVl@gR8TDCro!ek)BRcR?QS52{zqyXl%LsB+s0-wvLHGhztYCKakBzhc}eP;>I#% z-Q|x%5(mYS`#Ua0zl zAih>hkiwxOS-?YSt;{YMgRm>s=K6EGfL0|jPQ9-te`Kl;nwo2Mmt7}mS=_8f!!q|0 z@BX_23p5-lNMB#y@!)#A%5mBvL&6Q6yppqzA@K54{F>c`R?3=cCwNJEGGY?8I_JR9 z^suY}w-$E?{xN9U>=y?uHI_>WK2BsG0u9(dFAWYU!dIxTaKhMrX{FsI2@^VI-|=q6 zAf1(25G|Ocyz4DneneZqyCB9 z7ow6tqD7Kyl0l(SMxr8nn1`WcXeSB(R2D=4>jD4?uQwE%w$7j1R)D1p2jgsp z4O((b#5tVCf)PPPA}oHMf;Y=C!`a3Y&(=!g@Ih@wX$Zml8~REK~E{Crl1V(xSb^EfQDL5SFQz36B;z^;5784BRp<- zHX+SN0ZgVqjOYz_azNT!Xg=p@xtsG$%oGYp-{XKT@LxSNtQ&eL9D3^CgfXsbaQoo- zjs}0Zng^xR*#i9-ae2hzEGgz$>(1HQBmB^5Iw4gHxt1%Pq#^qAdShl!XK+Hdg+N?t zCcZS{+75sJzPd6MNTU$56=X}XeUS@e!mGbCnuSu|50gdU31d6?WE6z*d&s`|tw515 z_e(8KNQWY8YJ?cHfORl!GtgUBg0L~|7~Ek|z4;QtaoL$1Tj_Q#&gW6f- zN3dTNR&fO% zi7x+wI|V0epm@G<5jhkfN6E?b&=L|bGgF)YQy{U4L@VCuMH`U304)IbyA_0RdF}Vi zwv=uSbQHq28exEuvoIb~i@)-UX)QUxF8)5ug28%lpWnvTWs+@5row-Du{4#GduxIc zLX0ZBzq#2sdAxSt1~jAY=yS7|9nq-avB1b}_>2niiK65@3`%JnMS?NT#32UlSa3_= zU=VKEPK}A|&w5Ns*GFmUrB!PUOAUK%HBNRdeZro#dNMmd--g%g4il1o#|DG%c%SR+ zdP)4AaEMo-ufC1J^*bM2y5EQ4V7C<0{S#ll*PuD?5BPZVwnf5L;eP}ELr3}QX+T5} zp?x>~j5XGd$cKTCLm{SZPlHb(8(!10iG9}r@$^zaKqnUtZhx_bQOgtz2=0+p6(HPn zX$0M%Nx}&HaM#=|3|S7x{-I-HWJzJjY7a5mGLK(7MqG1nFx42@eK^o~fjKnW%2W6O z;eEyYGn7k^TtzyAf_ugCNvEo+81b|C1IX5pKi@nG0cyjgISAUedsC-Be%_>MdBLYX z*CF%7s9D@t?Jt&~q~XIq*!RaHYeDY|#2R9_5u}I>I4J1bL(xg?*tv=`ok&aeJh>}{*hDOTGlecziFHYf6A26IELtvsOwP{~qn6-r#!r5X zOcfpO)kwjYS2PFX$BE~we$$93t|;v?lb83>EsM^ZYn)dE%c+m$aT>q zR*=I%`!rNV&g(m7 zfQzgswXq<5%Z}{I?ghq2dc10owAug9)7_savBjNM>JathfBE2=Fsj~5vgB5k5RnMj zLYGe6WsMBE*QX64r-irjHu9mHzlETz$U3Y4RV`6N02@nl-Y%RdXb_gHT5nDT4aeza zl3)f=A~tAghvF#2QZupPsnwI9PVd*Ec|)0rpGguU=d4GC9hl&eN+5Nr&>FwT;Gd)_ zkzue^ydnN>%EF3{MUSeZQa^LxM0O2B6JRWG_+Mmkn9wPML*pK|@?|+ve*~JQVjAnj zZPABW#6O!}q}lmGnL6y9s@C6Plvhoe3u9|3VOTtUzVBTGcW^rvaN)u{Jv! zBD3EftX$F~S;iHzT7BaCDX$dzz#XX6h24e0m4BKwmwL*B$o{r59z# z#~S1f|DuSZ1(ccs8(HpHa#BGQJQ}1>ExK;L(2=lQYQI%~3ZS(}v_Hsvv6Jif%(p}A z1w%XC&uNz{0&StT%K|2&t1KVa7ht#Q2^pV|CQLF4f%%-GlpNec+4=24d?CUp(LthQ z8d#(!zY|l&ZG=Qmw6zX0NSj8l6}2x}BPf|_LHX2eLhODi|rq8H&x5gv{<&B?5RdWHHbCYJ$oEICrtxBaYkb< z{s_#!%rOJ$1kMjo-v%vR!wq0iWhQKEJBxWX5Y&Bf+}1rrwzfw>pa4!vVH!k`K!4}y zC2RxD=IhbGR1YMutO@c6^HH&hltR(2ApZelNFi1$FET{CfLudO-v$$Izw@ET=~`Z+ z@4{9SO&|{`?zwF=H@L&{SV~%(erj5ylNq;F|MP}eWxc=#_j#Z=6&9?2UVjL0H5N04 zVZ-;E++)fl`ii$JM;~Foxbmn-bC&SS1Z9-yj&nc5A`WI(L~T^x;4Gc9RuymSVIL{h z_gq|}?Y_WNzj&ivwwM~TFz&(TiZdDi#!b*|ih~$dwiGn}aC977 zj1veLCPq~E6q=tte-2xUB=~6)6V%@g*Fw_h2*vTC(vRcpeE52@yC0c4s}Y+;m3_7H z@gNR>IG-ZGt=}MCTyZT2VHHz>M3w`mI#MaY9i!J^Nl7&e_z?@|cbdu9kFLff&8v0> z%p76Mrvch!ckIPTLKEnH6)|)b;pPz4jlQ$m3f`{^rElkD0bQF{8*_npK!375jkUMD z1ParF{y+U@hpiO?aPO}t3M?^5yKqJ<`H@G@ReZo~c#8yDZG{-MxaUj=+$Gla_J6r2 zRLkG;#KhglW$9cQb&W7-{OdQwCL6I-t}pO!o9EeGfZNdd@5KKXbZkG26echt^T*oC z;jV{^mY%NrP(q2nD=ifJ9em$)NjxFbXOR+tnCEk^A3Xc0+)_>GfAdDc-KWM#D81n- z$hnr^kFxFs@BD52X!UUqLsxr#CMW-fdWeN(^WZQ*zCd0{kI04`iZ}@wngbTLHV*}S zFNgFr9q#rAf)5XuZyd%jBKxdVXhNd=LyfhNXne~V#)Zc^A$b;mx{#7bCXcev9Iw9!h(Fsx3L41D2yo*26dLjxaqsI&_ov@k~nHUpUgX|PAQWI4H`4lCj zs}uu~3K)oj6#oq|$^{MIoQmC3u&^OQGvdJ~PQKIqXw~+xyo&LH&v%d5J53R@@KiVo z*qI1ciPU9>_v%u0=rrqM?RDeqzU-Llj_Yywv;zKHNz(SoOT#p=%_Z3syl&b%T5Tuo zq3020~!z{vnhURnKy zCxq#UOXL9+pZ9QBRD?(QuEv5~NLPUzDO;3lXOxz;O6`Z7Sn@H1SOY_Bp-+UXgyJkx zO+GIi5uB(QXk`OUJY50qkKQyngCB>K_t z%Ik?~6v!dakp%1=BJbDP5Qt6i>L#e)qr*{DxJ-LyW^QX{FhS6#1RBs0QRNN&eG@fg zomzSq9-d*Q5TcNGp5h%S%45&-a1O^@{O0dh z0Iyw44DImH(#Y!Q*4HB@X!ulqbE#I)@rZZL_t`K*#0?$kuw+A!ePcG58y&$lQ?WgE z&Cdj>5G4g42&o<$d?CxLr}`T1L^0avM&SRDNK!oQhxT|#fv6vHuF2q}0VB&OBbNjB zxHKCLc>QfFzk@Q|N?1QK=71SZCXVCc&2>O=T
|h%2m%ICuwY=bPXTY`&i~1>tEbT8o?Cl7Pvo_2kwW-Mv*HsnZ8hzk_3hK zBJorO(;Am)tCVT8kfMjBYYaiHSyE3ru~UWo41GkH7@W9E8S3smnf^ZRR~=4LwHo#bT(^ zprN4^mzNi&up9ncb6vLO2STtwSuP?iszzV z;svXqfKJzSn0sgULlDy#Y^EAYvbmIGjz%farzf2^>F$hBQ!Q1YFxTMHu>@`Mk0Xj2 zuLxO(LuedFY3)t#$tHlDmab2x^ib>_n3-v-uB)3nymar&uOsAP7=iE-3wzz!08>zE zvHjEN?tGpaX4EspIHDP#5GE*s;bGJ2MN?j4d5FS-u5|b>RcLTPpef!&+Q>rnJrAj< z3v9`tQaBGWFvAI=@}DFO`>eAP>ZQFl*=#Vke#Sa1U7#`eH&&G*6D*0?MrNh|!Ki1I zBlYCbXCD#MMl=f0a-8h*e7C5VOb4_kac48^bcY)%3^TBry#hUfHq{PszkGo) z*<2iYRsZhINd+nb_G2Evkut#@@P^m>3pdqy^3&m~S;1 z>}otHu8`LcB&tnOam>w!4Wx`>vIRzvZ~If^yvom=yAq*WDE%vu8C~FuZ{#7-^arZ+ z2U{cI>S_TbtghakAhOKNSKS30SA()L80>h@;bRto2J+uY*LTqHMo{OVM4qIHwK6!7 z7`TD)L*7mnm(v+L5dC~Vqc7(Odqm_O} z{Qa1GUYW+I5vlDC?AY5oj1VSZwD|qo>6oDVPO6g`otlLk52IQKPR3GzPp+3xVX@{j z&gfev+K{7)sGeHg9|q!$8a4AFS~SSGnFR=A_$>a6?FU5oAPMtWm+vH}NpHeI`z3 z(Lrp~l|rII9d?`t<8cY;f?7+Uwz=A$E^vY~7;T7`zNB|7pBg^T+@X>1=-b-b3W8^t zr78!Ml;??-Ibw2h$i)0@x$HwGm$3$m4Xy$}2<;W((}41HPVFIhk|VvPk%rpuZ%)XD z3e}HU!wgfT3e~6@VS{J>L6IWtOz4rhuAakdrFq7BaJc;@w{itOQ4Aw)7{>uyaVl3j z+`X~@&6vJ!>c_0Odo_BwAReL63`slxd8)JM z)phgv14$G_R9+s%l#6(2VnS&WtXUr@Tyyd?D`pU>T=YGOO>~lNA8Qa9Kkpbjw_Zpc zyrCV;V0%wr;x4Sk#6Z_hEs^UJCRczXKDD|U2n6gsTTVd?xceyPFu#i{jcjgx-p{p* z!k>Ha&GIzl9xR)$dTlU0jmfi`jBHDFmJA>V`uFEh3<4V~3#xSDg4wMmU8zF#l5!;^ zV^q{9gSbu4RYgfX(CS%CG{;{6u4Qb^!F$_(uMlDOiEK2iJ-8$~O&-jje}=hk}?3`@JC%1Pmpr+wuQo z%-qv!8@$G=ci%&RA5m!+2$H++fN-mMGDk#2El*|OZIax>ew(F{a6nB8$b9% z-Ia?Ozef!1N&}B4n>Y}5a7+*r2fy%m$_?oh|Ki^2*jPkX;|3jz4nZd5;BXwSE7HE* zBwlqLJ_(PDyS2Cwl9-#TxSM?SM01O!R4R#oIVDzxg3E1FL^`2nS;Re~l;qoRq1Y+9 zn00m_ckc5UI3a^XpC4$HSt4z$F%%giL2bSF@U-7XVt@H+3S3}`xPD!FUjLy#4wxdC@w%5 zzR&{1K7aDW9M0!AenL4Pd>1cM$(>n_a7@$jI1zb4E9S#lUS7#lXNphAjQ;UU0cg8C zxP2kR524Z>jlMANxb-D6#TEoErx&2SXQ3f@0d67i>G#BHSKCkj&p6(4Ju5zoXdZBTr#o-SL?vfIm~D~ln7Vg1u? zz?9GoqDh!?y#LAm9p~L>=qG{x?xaP7#2i+D)o&cQ4jE8%Zw-;=pbYlhSAh|A$gR3P zTVB@XaNL%>0h>Tbx3bCc5L8spPG7;I@97GTsR*Cm~?*?dS+J{Q#rnJ z{GR!pJ9F^RR00htvikD5cStd#c5BT&kJi7>NVEH_>R!PKLo^!p@T)aL0Y)bb9Ft8H9(J#_xgGcAR!Lwte**kMI6n{)3;bK1;Q%CRzpeg!_gU#@W*e znFlO#lF~-|jGgFLSi_UbH!6RbdgcPINjDOt2&j#^ll9PAO^J1%T!5p}VyfI##Q zU*&N9<$Cig$3pM!Ue6F*h znAUDt&rt2AwV(sJQ=jV;T-Ors$12>YWQ23h{86^2Z6b8{ic>OSX65fhag3tX#3S7t{L_LWKq zDqEfmVyC%YrRJdnUyZ|-_zkPyy*$iVUCQOZf4@Nz2tF08Ze_C4SXLn1<==0!4P5G) z27^Vc6M)`_;crAv!oyo^+co9lYN9-NZNA-|{%9bx-raU~onJ(7f{p!&8yX3d1N#HC zfJYqJuv=4Y*5L5YfRe3p6KfjZN}58KSwe(FSK3H6+xoVLEZb15Ry2F+AZ2h5d;^te zuE7VZv*%8&4T{6$HoY}fd26V?XMvKqtJ4a-+peP1%5uGCLtSK5HMP9NU4L69g5&&m zExB=c*wEkrzJp~H7;}i*euH* z$cDmTDdh0d4_7ppp`~2BJRb!8P|7AS z%O)fPMu7Xxptg~w917jS%ZsVlsZ7k^3Qh-P zJL*j7uM$wR1g+s?CicW?|H8)0sSx8bG%{%q`(vFCwqV?n(Fi_05jewifjC9$09 z-@VCwkc7dXkR6Wmy@T<-ev7d=su7Zznpz6wk139dhIV}lF3lO1=GA{G>+9=Hd3jWp zK0cjf1)TRkt{Wari*2t|1SQIZaXuqbU3JC7K*LOdTHIh$-Kpt$yXKAn!cl}%Mf`m2 z!AT4?tmeh}hAB=7K(D#Y!uIlY(I1!hj+;GV3<^KTvrsZ*dTm%c!)bi?@8kvp3=Yr^(nieFdq{#4OjNkX6u+Hi+C%0W)xy&D3)5#`aLy4Bm zd;}1ifyF@n{(fv~Wi3K&oMj4$Qo!<^5?AAPBZ1L>AtfK?soswu7xc0vpxE6Y{DPQj z98BM&5hcVM*RRxC?kOFw-lMynu0})#g?;qPhtjM-YF_oAadO^^RTlvyS5s#@i7}ez zl9B6eBbN9sT}}_pR6FktQ{Z7jbpeent*of)+Zr@jKmggqD_g+f) z!I;9;ZEF_wSwAW&yiTWOD4usHUhjqrT3cI{Km-q;+i@ycf%xf}H;Or{!c|Z4VFQZR zWH&R@M9T%+=ao(9z%9HRfVIYr*=k12RE_+euVX(R32h&*i7Um;wlJWO(u;Yd35oF?g{e8mF3r>!S_R$<`w12S?#;hY*x*4&ajXPaF>5X07drx$qky!qI&cPjAp^E&qYkqad?L*zC& zAt9d+NHSUY&rC{B?y(uryt2GJQBqvo2125aV`5^2hch;s(p1REnwfF)%_o(^77DW* zr~e6tRtU^Rp`2#SFD#TS+wzQCH~#DC@jtp4rm+9-hyHco)1CSK+XX&|N+W?bix#P{ zkk)znpEqu$t`vMHdgb0k!LGWHYJDA5fMZ7!`Pu1OSay~32RS+MggO4Y7xHnUmp;U{ zSN;dSzCU<^G2CbN9vp|w0wz;Oj{S2-COjFnJ6Q&I_h+xcLT6;69ZJN8=@K>ZkA$J_ z*&)Xgr5tAzR0HQuI!3Je?+7$2^#fSf+%dYIdY-in8X|FUuv5c7%y7B!k&=NGt~A+i zye#~89j*ZL!6Y(OCLkU@>gm}~5EDCnTD-uwo{=j&`^+Xjtyr=y`S++LwVd-y9a_kd z+4*apRsS4@<;LSIFfh2;06lwod3pMr^<3)G&7x^s*5T=?uLvXKK_+lD@es$9Nzz_< z3R#+4oTn#X8iGJiRZpoCwHg;8WKLaK9n*FaG>fK{;&hQ$%_HX&;Iw zWcSWo@OUgaf)XpYoV=smo#Mr2r(mCmFi?zM0>7_Xu7;cJz(c6Xws5xM?$DKERf(SX zNXdA8)zq6rmjc(}SoV{=o}>3WyXW8&-c#_Mw&5UTTnq&T$3 zrY3%XHBR;>vYaWW$h(f!hf05j@1H1kk#y`SciBTYL?DFMIZSM_|@lX}nV{P{fY*lIw zN)|0f$tm#{2ir9)6SO&U@$Ycc>|P7EVeY>g-CX*XHiE*sAK1EXf4V#DTrW^@(n!pV zV28tEVO`(e@*XcY9$^5n>!TWo`J2B0M0PLm(GS;}4%-F=1-Uh^oQQ8S`|9wm^_kQe z-pbDj--h{7aChe*i`rRPodb8Y&DXDAJx&@IJYPU;2`S(<%9P~gISEliadnc>eks5Q zWfL%`#-UN%BmVpMZ(l(|NGaz%O8rRiab|02xp06b#YXUYfLQ7;d|G;)yhQW6juADi zRTRqtnYKpC@BAW9yh1`mX|fiJ2B%xzPU~3~SeM8QC}Mjl4Ii40A6S z#l^hdJ9k~q1qhE% z2%7^nET?)jIUjmx9ETKn^z8-H9emD9GCo@BGm!Z3ryipUlaEX8$&k828ZP%H->$8# z$pIHhFyrU?0-Hx(yLlIoLC%BD+ZOoG)-DIPr>A;$KcuGLdFL&@Pke$!PKT}?g99RBe zcb~;vt>R)ROykBEz@CZQH$M~C@b&V)?v#qW?KMh2z8kfUx!&Dp!?bxtKq*`B{^o6U z$G^vRkS&!2LKZGTK4j$Zj-$G|`l2oHFa}ICeV7A8?Ue%oFV>p+$ur8Af-oD~4wTHF z(il>_Gf?BqdW|{qd*R4H{Dbh(89A?90^P`%6C2=5D zHqvRq2EW9N#3i?=4DY#f?L&4VjEGC@3b?>^ttYTudOl17l*@ZQJ z;Nq4gBG&|9_; z)xzsOc*Il^NbYHHxbPE2j)j0TRA_0jRiD!VgW-1%wd%0unwpqYbT6D(SYCelbbpB# zhC$)`qU`rDlf0Mhb-E82-EQVFFiz6cb9Sz$`H4=UtoW*e{9wk)pU`Yx&dc##iSW=w zilg9>MsyLX^!fSO+TFeR(5baSDhj_00ME{qCc6s9R{ci6e32JSr-HahY&a|sE@r&- z^i{k03mJO}hSQ>$v zlb&7;KXk}mNS}Vp>Oc^|>Ak~3)@yLJ&=uvE$3+(H8sjbt!u7XGlEE)8FKvKU61m(j zh!bu%A4?u>YHGUd?Cksx1w6Jo?;U?>7Vv0g&P*qdKo-;>@hR`e z+A!MGfRwL&(VEB-C;@{xrMJw?>PFrdgE#;A;^Q&%sd#yLg@KPrErFH{tCo@d+hwx8 z*wI4e@aosr(rTgD8OurP_uObBBO{B>Kj0@tZY&oKkbkSHfe9Q(+ZK!|+~aon>5KU{ z{;9q>lDGw{+R15)siRGYwkd~W#H!A}x=;IIB0YO&T`2NZ+5?V`(sZoHZeD&qKF`h~ zz_jN3AO7)J05-sOPEO7&kZRdfRrRiYcn1mHGa{K+h@+j(vxB-)fJH10mut|dMM>H^ zQF%_90)zBQ4NvM~c7C1-l7Uh`QfEFM^2O{!vi;PGt~4)hq*==EY4_e}vp z!K*W-qK6?3w+zPvTX-mpp}JJ>5)Rr_NukM(@+)iC#d(nTZn-#cW8jNJAnp29dG(f) zr=B2N%hz?7%)MiAYRaJsxY6dg#=AgDWQJBxNgX-)$S=YXFMcwyn8Gg}dQ9R9!$b&@ z!n&cVt@PO5uXPdcoZ13UdkjnBY~lw3nPhw+}k*`~Igkl;IlADSp2Bl1~f zTwwK{0p&@Z<5%>pVN7Eq&7a+pK}4!tPr-eX1_=y2jMdjSPgh=Xo69ylHG{U~t*?>! zs141{S|>~Odu0d|-sjb6j*_ucZz^d)MBPsu|1AlXc>!uU=LG#@#ygI*vH4` z#m%DygGrJjQc9ecJpW!;IcXt!X@BRya^~;@F4hT8(830u{Zq%`g@<;s`E8X!yDq@1 zaU9O|bJpAdpOnZY6sFM1$}7qB)fIhhco=z~Zhdx}SDgDPA8pUSH;uS@hC1G#Wv@;RmyxCpba%c>?<=D6&Yi zc@!yuX-YPtN00IjIz4;waeh^?oFNgP`*y3a0Y;C{Q`uK4=2~)i9F$rNy71yk|q#u(at}$2 z?AvK!ynRrm;Yq}IS!4Y&e$zv~JkGA-O9CIJCGV7I>9-Rc+ywb`oG*}1gC1uasu zGQa#`hR?SY8YzPAmk}2MGLGSD9TCNBj%3HQ|Lm;#z#!4qwQ!u_CN1o`(G=Os1YdD( z-n18HPdfAqBzW5P$9g}dwQK?VrB-_m>ewE-S}0L0>vfNp@Mv7sR~+vjHKZFSpWxia7@E93eq0{~z1ok~qn8u1BYX{I=a9Sxz+x0om%%k()9mK_lg8Eq$RNP`Wwpf)L|&8t8@@hBn;Qkq&+&Sy3!JIY0cXh4)fZ(VocBBLLSuvs z^nVIJ$}Gg#rhdeg0+lFWc;_SdADkou7QQ?)Gm|H%H}|%)s*{WvB_>l z?SC}5Ac*d?adT^$vEo{O%fw_$9!KwCxn$lh@+3|JPS}rUvREUwHQTyr`o8cQ40QBz z5LGG%;9qi9*0?uKHp zw@O-uw z7Y{sFXy{^lWDFg+h!s^ZhQCV5d zt1fBXyHJDcK@$_3pA#`*Vn5?>wb*X{Q#kHXvbhwur&~Y!;K$R#QGjYhEy-; zbrOAP$UJ`U0>HxP*cc4w&YuWfK0Gv{&x|!_OXoMe{foCZ8Egaelpw?eP&w>KZzZIb zz6z47lfI9vue6&wx|-^UZLa0=tgXx4p6)KP2@E~9T4-g5IXF0e znsKmvH`k#L78UpIXtLT8XEM+e|N2$lp}ZB2LS2*cJqyf%EIuk&-_yoxY!JPYiINjG z7~gDZ#K{Q!q2E7#UUk&cB5KzKpjSRqG2a5 ztULP5_c7ycz^>WZO^N5|n*VP5H@LP**8N_e>APQ_&qQ7T6>#$tbolUTOTK@8VtIk< zqsHHe=W`OPMzC7Na8zo`tYcZ(rx$bqGRWHYVARgP%gY!5H}e}CxqU(OlZ~9be0O|W znhwB>#sjs|VAkz%pR}+)Eb_p6MU4)JQcpF;-_XP*f!~wmz7``Oa*f11HYTC3ufM27 z`t=pY`#|0(n8{4^rwSJul-Izp;AirCt$2TCq(xFC7x#v3nj>K0uNL$N_xv7T!MA5& ziAr!$RDoQS6Yy+S>pssEhjQD@mpboFXn@3>?MDXj8Z_ZFcN?Z}wl7+Eqwmrb&u+eB zmF2>GTZ>nLkzg=74MPjAg|=s4WsS43vYKkuf0sbZXbO@dc$xK@j^{yY+F%_sy~okF z;euR{!ZG;-rb=sdMCHQd^By#ztH_|QQ>ih-5`KwL-fGIQ?|WL+2NfO8V;!nz%VBe* zTR7hYyPo;=_0xTj5bnRIQsJasHu@3ImR0X=Q5wp6Vb+&ZW9K@&{#xH_D z-sws??~M;UQcw}oX-4t>N~~bqu<7mfmYMN$%Y7T#-O$1dT+YKCiy$j+%C+b7Ih<8`MRoPr zb2HF!oMrIZ`+~$Z@-mn9H6cLWOn}x)MnXcup?FT-4*zyb^;m5d3njJH$T*{JD3I}ipjoX8S25R@KzlD7o(ptxGoZZMcY$~f``2N=r{Sgid}B0wkd00+$n^;7fqU1B+PZt@XoPuuA=-Mcv)5xJ{CSvBNawdVfLa62tl4h z(MYz1$m6!A@+8tRsZ9Y3LGfRVDm}=t^@aqdh2gxnA_ll;8@yFvvDKZm^LnD4PtOZA zy*NMzo?aW$>D!VdjPT(o1QYr}k`p;{VSC=Ddg!4})$pSVBx(@6p}{77ND^$HT2~hr z(sl6ujSatli8;q-r>9M8K92mgTYOoFj1Hum;F$P+fWQaEm;-w zsucy2K}%BThled3H>7nU-}R2KLjdr!fDTGS4kRJ0f;cw$|G@`-zn7;9Fx{yS#`N59 zBE`z{GlpbkWdXgb3_xcSuGs}Iy<(;1$nZel8=vs~Kh?v;NOsVV9}0!Vg?(0B(F27z zSd5VzTx@I(hXCwU3xP^4(DY?(*=-O{tMuaH;`DaIZ-bqS3-{P8BPOSVjoe}% zSuTFzvH21APS}nX2`WnoK{nw-LZLrYM4HaW@4oG8zBk~1?&S{V7&~B!Bg73DXBl1H zbii3MTE>%|cDuUpAdlM~UluyniW^&E*Vj(wZR*96MARG|AFIP7ASA7>>Nf$27pSLs zjd^)nrI`8aR$OiRq&$yMr27~`T)i{ASDrqB&cMu2hQF2esiG#-K0hH zN(Lz77@&rAb^zh%-WkZe&jPBoN|29$-f4IS)K?AHH#ZaN#dCxfn6*D!mIBzJc){iM z5F89^s;{5<@+*z=p{}m(zgNx_jn#FW7iWCYO~}mbm;zM#;q?P87^nqf@}r^%+oZ!w zebfg;0Ej3(1#Km$TFGhvRnh}m^y^JoN%rB$__!;7mQozp*}#F@3KkeN!#mC+AV_L4 z(=KE#xgs6+e#Zj+1?oW9)lf-6fy>Oo;&5*=r&>!xquF^+sn+}6$pARhGnvPd!Pv$I zh;ZKX@-FO0Nx1WoE(1XV0svwN!I(Zg8FnBtW9?0_inFt`M#lKxFLrjWfWvwW`@w`b z%kd7>rUg#;`Vr}z12MC5-++S$96G%PzxNX&2O*is$;Iv4k#A7}altS3QPR$i1x!-K zFD(Dd(@+O*6#<2(XKE_`_v-4gu;*3j>o2ln*=|y}ZKsZ6_SD#1qjH=0l=h}gy;n4R zkDQWq-W7RptP_Fs<4xiSEjaA&vy=#9{;NcQ+4u}tL|wp~F3-&seVYAygXHFreIahk>WC)KGsQziHSjP|GKpvc#EWtm|@eJZ*q;4>x+C` zT>e?I&xZfHxcov=owei#^8^Q5uBMijSKpign9p~}HQ_ITm?p4`Gbvak3x2G}Rl zuWt*7vdm!0OZ73s9(7Iwh)h;Z^@ z^oT=r(0d?YzGAKZ%}8~Kk_&4F32l+(Lx-x5!x@6wXI9jFy8`78GLpZ(t<|9vul*rN zv8U~mc_OZ=DDyr;!J1=X^vV{E@*XB`XtsHa;cXvit?ppHB#zHEN4os#J3xl#GDj#) zI}hU_@QHkjU(!Y6oW;^g3+C>~;~Kx(z=nrG`*W~UOL_UI@S3kaswC6C8Z?X+%+n^U z8SIcC{3OjLyu~&B{TuU@4};z66zhIYbdR@rftymlv`@4+i0r6yddFnm{yuOBhI_Mn z%Xvo*c}v6w<$>R`M?XEU;sC|DLibTbFnstNetMAu0{7-kn*Wm*JaV216iU1S`!56s z9wwjG9)Rc&R+bY#N)_bAt`A_N5Qxe(m+ zp4b3~NhEHIwc#OGbT_{Ky{sxWOioV?9skir1&T>Q7%UVM+OS%f1U8P4k&0$ASBNhx zH}{)1Mv+z7#eH_zyteXBImMrOgU+EjvrS{F%fY)P6u3MnM7(ojHSAylau8~COuO=$ z4tut&&bwyW3wS&cQRAcG<{ocvJ>sOqWM$jn*RY}eV*Od!HeXx6k&Xv>L7vn^=UfB;3YhQ6;Z+)F5s9czy z(_Nc+yHE2s5Vi+I*ef-8Bk^mie`1DO3S;atXaxcNkn=ny_%hmE|agx>@ZV#jqh zlNZTr&1%C+=MO`wzZ=9c@i{SND50b=5_R|R7k%sU^Ia3xu;6S)Me=Jkl@yjFX*szB zf|O7cD>%flGW)(Pgg11yC1JAQsje7>sz_` zxQLFK%{ni7X4}OYF=2Z(r{~EPuHpL`4UOlW8hr2#lNg(yQzkXE&X5lnGLfmXKqB`i zquiLY*ND529VZS3bXG^fhron?k7NVg&^8e-$?QR$jj*W1vtaez@d_JN>)qY%fn6p# zvInlP4?>(qVwCE&NCofp`nBF(j`3vC=#b3^2I*21zQ(BfahYM z9lX9VQYHVEjWB{Xf(1m^_L^i)ui|1W{^ELduv2PWhdX-9*}3bd&=U#%M7^Q1+IO4r z=kIvz$?c`Ae}@;x2%9q-de`^H&w9~tUC_W!UI^@JiglAT*NK-qYW!A@7la*^lT>O=&(wxW0ZIS`;B3GomD}e|e+}crCVGrK4n4=m-w~LTkJfF*?fm z&5#BP-9i{57BfI-Y)k~TfPlv2+`xAk1mTe$y-h$cL5z0JQ)0vxkVrD>vw~(D(xMb4 zdl^m$wM*Z?9rZV(+~J2*hi#GY6>xUqgnk(Xf9+{OKVL%xc=|76be?R#DIflFsBNcO z6Z>^`S#Kakn@X!|o!E_x>7@|MIY2#in-7{B(f9e?Ck>9#T@yp9qYni0XiKg16n9_Z zLRzwOzAoI-kRKDu$&TlZ62I03&e<${!?eF=WlZQP!DQHpUVGu`rh)lRf(I|C>mj5hM=o5)M z-84-fh`Q<7y)j40jACNy6@I|Ut%2}@3#E)s^9SO;Eb-{4*u(zH5mU;B`c>2c9V?bP znoVYV_A)Gg#8kcZusqTS71oM*fMJ61EhqfU5ZlqYMGw{mOI|pxYS77LZ2G7Vcb-*A z(1GMo$O<7gT9V=Lz-yXO2Tx(MTjopf=M1L651bNM>};3=7fl@h9H_dny7H z#pPV$CScirG3TE|Uy50EyeYMs{>^QZ98G10j_cBQlnUi1UPxr|PFm$Bb^Kzwk?IXg z^jvSsg+lr4(2|Km0 zP7I^YiFHCbEP@b`CJ?tp@GMu0f~~IoH2HQQn|Fb&UJ#du6xsAUZ8kD0hi3B6xD&BC z_;g3n8DCU><JpMgn&|YSIULu%U&~U`yv(~IS0eQ9jb-5>oy=6-g+Iaq{LO0|IICo~ORk80D10NR34h{5YyFcSB~HEjXhQNN zuQH$bIZtM0LN6$6r6+(P9jq3=xK7NPT9_bzeYW;%z1*8!*1C;TMf~l=p3-hBosai| z{wWW0PVKa=(&By+NNs+bQ5eEokzAN`q*=OPOn?{IjUl3^ssCU{{Sac5W-RuKyxEC) zlAGIdwUaKbjAe}0&%N}Rfl2?CNjD|w`?^eSxAEQMpO==vR368``>^{rHh%hQIv2S4 zZ1N2n8t)V2MoFR2-~YXsUo8k3>3FJlbcjIYY=eq8hbAmoX-FGGk(1-_`RGzO^F`YD zvRzqI9n~1#VcM-aq{7H+zlRm-_=c5sC{$$tu419eG@k`Rmr+J%8=AH}3gc&~@rcOq ztIKpqP*l)RMfE{JOf29XZuN*o>K3Lv&qoAj2NFlB19AVM;=agqY3EvYH6DJ5`sA1| z0iw1puEK9J8X4v<==&&_(WNIngZ0zHjm_^FG7w?#?U&0S{EFeP;_;f|xhJS6CeRzw zn=4%BFtEotzpiJ=t4ay2`Z?&jD5B#{%23--9FuE$c~gkP%UDYpexGy?h{}O<<5Vrw zyqd4|$i+R+rwTbxp{6pT-j%OJQdX?v>RNQVGTjVH6wucQyyKoML_I$f{xjIz7v}TD zhrf0{OubM5Ol+i<#F`O^ylJ1_|4bEencE)Ev1qMDS0f1x27&mJAD?4jw8h}Rvh z29vL{Bl>7Q@=qyAM5O?))Hv2XfdtdhBVpn2;eoWVa^cO8?_5|NG?C`bK6KQh4Tlm4 zFW<6_wQ)NBqV?pa%>E;?a;9^jZSkj!xqwEH2yAgx5E5TBLxW}MxsZ)+aA@F!Qk=>E zr!@8}c9TOEC$TH}FfuZoGDg)ZUIfFze;Iwd)NC3lOkQa!rP?3^YG_StaBsuUPb;_W zr>>|1A5{8UwvcJlDH!y9kB05%#cY_LC>%#p+x;C#jZSG*Of%$;jEvkjp$c-RV-aR} z1K+o5+y>N~)F2|#elo_g_?fnsyvt4zNGmoo{s9((`A!QX@H9nEEy{ihF%AxwtqY>! zpF76f&4#!BZ94(`3a?@;**e_=rDl9im z^6IBsjfS`2La{|8OuLp?s(+IeVm@}^qQ^~T_t7P`hNqFDCza$0li(*RXRRs!TIJjY=(1~r;JwqV4$v5)2 zeA674GjMN;M5eS!v4i>G|3SK9kS6EISG!%y7o6M5%SFJYhw)PJ?$>L&k^UhaRX5jj z!`2hIXwJX4E${pkmrVUV61Y#_Y@OcV{njFGGm^W`S;pLflBRTMhEqa}I+k}go(m51tyy7h!&rQ?Gr zD&3;{%|Boq)|;vHTv)U{j=_JT-s56C^nzn9eyH053B&YQvbcs$gfq)mI#6ED6$2XP z>1a9Vu^TU0v~&X=)4EIs<6PfgEdViu6q2eL!hqJQi4K8%aVz{B{*Gwh0fTzHbQ=NM zE^bEf6&s6v#|vaDC?S6Bre9!He0~ExBk{+)SqXb6OLKU~#ADb~A zcoB7jubU$WO;=`eG>+kRd?8R?D@a^+VhFiniS{ra;y9w1Qcf(doK4}`ncmHGaYPzxYvOB#T4kZZ z+9Sku5%K6X*-$tz7wa9T5)evD8;fCVp|e4IAyjN9Ngp&^Wr z1IP_#X-a@5bk2Nu1zZ%ygE=Y2U+22~Pn7m+ZKxyT%`h#oY1Opi@K6@;f`o=E)h0ve zVxu$M>uf_R@q$iq#S2|WMs#DkT6~_LUa*1*FMLsw;z9~y;5$r`ZwbDCe?^aOPrH*w z6Rox2PO{lLt z{Rcg$5Pnr4g*h%o8;d*aMx@L8WO;#0(L2E33EnC7>n{b{g(ExsBxn!L_F+X0O})kv zpZ-v^!9CkkR{uLUwttX9C7I_i(_Jv;CHRGZtrl8mqhsw;*sO~& zi8cAB-En6TH0Y#MQC{9=E3VJ_2K4XGP{GyF$>-JoJ>5s~1~Qy0!cQ#_1_s7wz97TC z($-^DyxN+VpIuET(=Q=6ZNr%T=5c`ub$+JjjQGD9Rp)3ig-3J4HqUTq1i$75lY*CD zqq0j90hJ>i6Kbrma^`vTdW%OJ#4L6ymepOicjrkr^M$~^a_%T*f zUEwhZH&IMs$~2rRl2SEl`289}u|Nq98lQ&bRbC+Br*J9`v+*%IEsX{&6U$5hV#I5?WAE6aht|NRk{S2of6+P?9K;66{=x&j0401iPA zhAWpZ+&~aYbp*lo(o(|}YSR$~_z84X({#LH;EcDkw>2@hGR8YTurtORyO^6Gh)dV4 zka`9RVcH8bYR~ox{le5eAs@A-;gLPl_qEHJ`2DM=_~H18x9y~?P$$M?<-UoD#e9^? zFFx(c&$7Heou2+YEk@0R;Fq25bAQ>Zkjt*{Q-V?D3FAoliwP#Z2?l@Wf^R<$iT@bq zL0r)1IZ4Hf2xmeo)y$B*D>tbhMWNGY( zH#9aiw~^+WDJtW_o8Oh@(hyS;QnHgbHZ#BMYHxhg^}3potEJJ|yIis|I4KuNSisuY z(E#saZDr#i=_1Xwv#uokH+or+3%~P-qop*LrjiO?-qzk2fBMwvQ$hk4UCi%`aLM5C zQucREByU`}^v@FTmo%4|qobXqprEs}^C@T1Q?~Y|g2HFdo)r`l5fl*-fF}eT9@sb< zxCq!d97UJ-yT%1$2P1oPJ4bU{8$7zEfuXIFqcj&6+{gd3I4XkwTHVIsA0_}h1kpQ! z!l#4;|NU`CbCdsv$I(0g{W$uvq>8zVv6bcpb8BN82OvS3OITP~XlJ$mzOD6dwzTAUE;7P>Ji>SgA_Jr8)Wci2M3r8C<$DNDwRKDJR)C;jl-L@3xW)N?(x=U!mm zn{eJ>Iez0r;H@f$6wk88#g!8w9$#+W>W^RYIJvdfZGh&U`c4#A(O;EdeJc1NaeYRU zYr@5c%^AdBTWVoULf3!(kB~AtNfp>}T(xaJJp@ zkNLLO!^k9x1r3C_KPn50A40(8U@=H=>^IdX!W)G{W%K5p{oNbP zh<+F_l3-LStnh~?UVYWSQ4fQNU*uy#njb%?IVqv!2LB~qX^A0f>#`&avQ@+z;Y)x=*x-#={&(y@$|mN_dO zVoHs~+^xnTDq62#`9`3Cw5t!KrY`;}VXu*=*L{?H$!H*--#U0OOk?T&SXm%S|KVb| z=H5DaD7+)OEE2Ai7dF6qaW7gAg{L$C)hR7FN~FO_D=1|9-oi$ig`~`bTk+y^&T)A( z9;Ke1n^qIO@iJ6Mc|I*jie2EF>Vvv>N}K*hw6alU@U^S3XS)#)SnE<=ys8Wz8X?nl}nL~#?nhb~fcTnc5ly|&TRd8AoS+Z!QO=4|Gec5yhu2GXgK zMSo!EDBfB&R&^o(@eDk$QFsm{#S;(KbMJJ=R}-!eAbi%$6dCiT|)3E3vL3dUCApU$?TWtjma z^rR>}K@nLs+Hok{M#PH>nKuE^9E%}E!P>Aa$mKa1 zhQD^vpSxLpgV6?kxnx}wX_jNDM43z(9n5(slL3tH7mv@CEjJdH z+^T6@*v1JLs@hbayqbAd_>rEBp6vD(Dm`@yByKpE87VMDc_Zxp_>Ri!k))~f|MF;* z$gpTJ3sPZC5;0$2)ffAfdZm2c1fB)eluBZzCHBR>Brn8wc|L!`%q`qXCN;G;WyD*T zthWXrN}5;Q zUV+2@+WU`^9hbaN(9(D$m5fIuA$Sn-8dS$DmITzsBtah?pXgH&`J)qB?p1`}Vai0B zsZ7$4P-X`;S3+{jLGWm#eaoWY;zB8`Wu=ea9_xSF1D-(y3=eOP4VO{xkPDY-<)9?k zP)8s{HPnrsr$6$DxA~oa>-?Wv_x{D!v*65$L)S)I7@luH}pX2 z{IttT-98}uzww0+OW3=7@*NhFx$FA+!0T9%Nhv7?G*;B8aA+RpXaQ1rK{Ppvq>2RW zCwjp+-q4yVvvYo#g8Bv=HKG)m<`9Cl9l%c+(PyMnaz0|Jn>TcJxt}vB>&aU8`4HHt z_(fpiASG`@RDRbN40B zCO4-Gt2|iI67}-E2)k-VjX1Z3Tp1+R?HOUbg;U-GLO6zAWJ(rFNF*3fk2H7lHLPE@ zV8oI*fw;CeQ~nB6R`S-esZ5!fvkN@>7?KHG=PlyB-bbspqMh~OzPo*E4$a(tknay5 zw!AK9Q^*KjsNV}OBuu!NAQpH{jQ$e#rw;G6o)-ouBj2dGF$J12V)YNhN~KqTIdZ4H z&+^`qtq(CZ85q$n#}cUEPTbom|IRwmshuG58&$SU9FXd;OD*dAGr}KEbhq>9RdfRj zbV}B;*XSCqb$hBCY~ko+w}E-3=Ybxu649F~WW36!rVZw}9@Km?iIC3#+mi7`o#br( zsq<1*pl2Mw&BcE?%i{%n2Sg=hKACs8CJS+1_dN^Zt=+0>`DpEG+r!Ma($n5o$sY4D;mYrTq8MgV#LuO)wF63a8yzc2e zb%DJDozHwb(jaFG;hdz?j4D!s^k#JbXB;63Ue$SjR_5%*E+3ZwX}V?xc=ZrnE}M+z zuIT_#2es$#q#vJL&LmRdE#c0!sDujn;p8EO@`{ShG-+DA4qT*%i&j)gQzt=rXNSn( ztnVshVnc--)S}W0D0y`aK~uBZG4tS*E`RzpAA#G~BpOXvU>Npq|*Z*jxPH|QRV*7u2 z4Z_2Ab8tOY5odX&-cHdhXt)FbQ_c6lr9!)TKiobnGZ^%jiV0u1T)Xm5bPz!8;0xQf zz?V_3hQ*xtvMEO8Z5!c&88kF_-Im>!e&ljke&ut35VQOW=Rd(s7EL+>5Ubgk$zWz8 z*=*gwZ!k>Dn~ab9t&Pn*3k(sb;{t7l8&eaz8aVYaX7Tz|q+sl2QpF>JZ(&qfMa4uA z>|EOzMDBC33GcUkTEVwlvE~r-I4DTq7bee@vUab{BcSU!j=f9pR7r#Cj)QU$i$7t` z_`B9J6j*c#lzRM>)sFD}^UT4wEPaK@9}JsL1G5~&Rh!FNMS!YDo&-N>0WnfD-{m{G zQn!jrh4V0^sE6e{STlu|?T_nYlA*XDVZCur1NK|X9)pZ7j*FgJ6Qx_x8=S~4Z``M5 zlzmaaK1=uj{}^P5M^y$akHcN5U7DIMTHxAMAVTOzb6x0WTs|Wc-2;^JU|EPiLE%ep zxwu^QHP3oI<^SQ%$ghE=>uyZtEug)DI0kQAD^{NL@tH?5$?_=~&vIv*k{{^@<8jfv zZMHt};f;_&qBUbbsrh;Hbl17KXx~VAw>|otpuJ4Vvr%Vxr()TebR$?rWf?IVmf*{V za=+i9bh(g;<;Lf1+C9ySCpR*PAED&qG`rIAwW+F`j~}m(Ss5=n;tCEITK)KGT%fSCY>*_3zn@$cWe0;w-Bntt^OU6%5}^_T8{FsKHizUf*U7n(@>y zd1i9C_Xf=g`OTMYwzd^*XR;xj^iV35s6z-c?&v%mcUUXveo)|dGODzYy7GQD`8N$V zSs+6NwG*q{EyY|XKb9&CXDmJ@Q0YH0 z-Ght_Sl87b`Ny@#DnMG0Z*Ql2~aI0)xK)nT-w7Zo|k$D22SF-U?IJx(5{2_^DQz0){l1wTZoTzQbc?aZ&Bc*7N zyE2@dgQnzYf+hS)w$*Vfph<3_L9aGSLJeti??Z^EgcTBN?mrkU zY8Zke2!LlLm_pz&^>Rs?O1KzC_#^ZvY9rohZ{{I0MvFLj46m}IzQjrMb#px&_+NV| z2e%xW0q;hRfA!%CR>Vbu#->%73t3i4J0aqVLFj(bG5uyW|Lx|JSVyBy z!e;V3WG?sNuKiAKEq(b^*=9q;U`r8_<9kOazCy)6>PF=?@cMLmynmQy*H7@R-LPMv zN9&;*D?zS_7U8(odAjCg{)bekKRs(fa<_&v-nz~3%CoJXM|wbPC4!>@6-i!{169+r zlV1-aQ_+Xw`Bk*ueJHVJbQL{ysari^M_*!@^j3edU#AP@N+%Hg)x;}6ACyLtFF2@K z{8J!Dm9-R`yEW%{G?w6qj&U>yNhw6xrZ;EmWc6&9#pxwNqI9yrdS&a=5_3jy)&v$* z=;!L@f0K1QfJEc9#!}#2iud8Bo|JD#n)p>PU;Q8#t?td+e93cK=X7+!yf*2hw`<2- zL;BdzTmzPwI0;V2EC`&#K`2uo&umcblmaoeJ{gRr;zii|7ce9n5R}dE%H6$g9U@+d z0-YR9{A!r5F(BsTpGxfa!HU|CsR%0^|5Mnx5w315>U9y6WEZ417O$t?d9)6O{oz?f z75++--kxN{abjz0!uG=i!AsYJJzU*D$@oFmIjM6WnWm!S$gN|))A$7?%beugcgmTZ zqfb`(4tHf2d7Wh*!(IcFg7Bwec_+e=8yZySf}P2b;ezrsRgYviS~Y=%=P5In5AB@^ zW+pWiXWa+}uhyHpxi}(!|9gIX7MXNk-n*^KNAkc!S0k!leTjE6v5V3A#y_<+z_DU8 zpPt-Y6w-{|o0(Hw7K782+*0qHBH72^Br;L*5t??XM+n+8yB*|RS?ii_9n?Qg37k@& z0GBeU1wqvv;uNAB^G#LtS1!aTpsKxZc5&mx7Y)lMy)09VYwf*yq#+S_0)?yUt7(_v zwlpWHY*71laPSSF%o9ltYHtX>RDZn|Xv4$WH4cHF9LH2Jcrz-je%)McdhA&l_6S$I zR*oMLwmXveHN8cT7(Y<;TWOORnEC|HZ#Y?{>aFx>FfsA))8ULSE@u5$yLfdxQKraw zK0s^tRMa)To6I@iv$RNO$-2G5cX>1<YnU$z2>J#Cy&t#fLO`M`-pcTKrH)Egi#1C)}-k}zVja*B+apZb4kKh z0=sz{jOLZ>@cl+e)ot0|Xzf<^NRnG^A=aOkEh^DfbZgrrVNjKj#ayFRC*CPq6X9h} z{23^gPQ5I1wW{`@x8j}nfj)uv((8|fawI?F$~7J`llmEd=&t+OQ&gGt2j&*FVLop2 zFPoTC@sn*Bfc<&^xsaH2uj-keJtO?+_L9d|0Q+U+hY}=cMHfmtj{t^eEIhu!Pg8TU zmD!^+n{oMr(0=0D%zI?$J-^@$%gD#ztM*&g=-!^M-II2t!HC3HSZXR?z@Rp%i^M>< z`KtPR;pB;QYFj{Lisja}CA z4|(}2?!xmm)r>GgmAkGo;>&jP)GFxTeq>ylgwdJp*No)mpNK zLZ)1}e@){zJz)x-XY0&}Gx(!LbrIV%7lO4%gijmht7X%5z>?}{2y6`K2lT@p@j3+N zCTTO|>?YiCyRXPL-bWQG@)<|;ZyEX_?PV;fUM+%N=VrKC%?pgPtyk!SRc5ABya8vgsW3_6w zJ31HLd>5A|H%s`yj7|b_wEu~gCOKInokBOcVeqv_YrOP1Kp)Ueeu9+k^hJXX%`9=- zh|0-B%;C``^5bsg0aUp|XG(Sx*8=5y4H7uM**8lz=N`$7H)>y=d~uc+ar6d%IDSmU zyhIb!u|-n<$g|csf$O_%wq@s<>y6>QJ{5s8%G`JO;Pmxvt0^8gRzw6ceNF%0%Bp@I zz9n>o3{(YIZbl7^T+5bKmc5BcHXP(5c*QTtKTsK4Wb+P?i#6#qBA-I;Le93Bf1hs7 znx5~_%=?a;FEDUb3WDm|dQaKpK|U<=MRxreY&jNy2dRt6Z-gpM5m^busdY(|dRm;GSh!-1;4*-7`l?K}w5M_3c?(HrQM8_r+}0J#%vdmpXv zMhsZfpU+C-Hrn^M@Lx>#vVy`<_|fdW+dd1aeoM@qXRA)B%6rXKR;`K4?FJVZOBJ?9F_g#!W%iHtQ*x5m?uN&@nTJ;3RGNJrh zk7uDE{kHxIMQiGJj{WZSjl~s{w^A0^CbirmFY0J?GZF;yH6Vi*E3p<+iMpqJo#K>R zFUQ?}`=<~#hP%^1sj`xU5|n+lX0R-7kr@Xi#`5ctk0n2WqBaR|qr#6iud`SNh;ZyT z(Pgvi90N}v;y)OMIZ*ru9vK&ClN75zZ&0nKP&PI_w86X{5uKhoMMIj?$pAm?*_nTU zyP@D6W51(wlJwU;V+cUVRIeu$9}*yTk$qv}3riA^$_(5n^yp2)raM*tXxqJ-yDo^;KnDuI+;yC&)1lmI%Y6N_gv{-q%y|)_1x5{T4XOye1`&wz^p;XVMNr1AXhI@&Iwe0@fRX$GHr)b#`10i`OkhP&bYX);Z|w}Qx#pk- z8!q$=Fxk)|y6g@2;CX}L*y%6JX(jbN>3sB5U%_(m1BU50fIf6d+d_wqY%=;a0u}xw zeC>-suLVhHFb;q*TD86XuxG|y@|cgP&o2*4rw$M?85ikyeD^yR6IfC&U7@lAl#7V8 zT9CL0W%?aY7MULLeM3Gas)N!|^bgPy`UHj5H&FD1ZLmy}VfvN{DC8r}FlhO#&H!xph*zqwXl~p6U!XDzC0-fg!PhUn-MlzalYoy@+czKEdeFuBfp0uy zdLIaGFR9cuuoug75x-kAV1f77gV(`@TV9p>#KUW?OhAODI;Md_J7u zdx#95-N732aM>tUb3>qRcxzAY0b2Q58+4&A^p#NC!rFB1*mZ?zgH&&V@5|*6fEwHf zA9&#R7bbgwI+n3YiGd20!W?HReq@R%4PbtWUdQ)Bu^P!vxzd-}6g76+$J0XKP?AK-~L6rS+5EmVCWkacXA=e|=a zpZh#qn*hyHc?I$H%60nls(G$id1J(jYyl;)?Qv9Sk@M@Iduwoom2|9y7?Yo2|8TA; z#vbry&2oue?goq7g)BM@k6w2aEdg51546q_2~-!ge6Dutr+SwJmUK77iFr}|G6y-i zq3fg_cG~%HskOls0LU}}0cWD*r=+O1ky{0`!UE+kPPDw#I0EW?*cYwL!tT8&qZu`n&WHwbPlDh+~S-&6hlRa}LAZk9d*X zIj95zMD}}i6p{Du^qf^*JR>)0a+=M2lv$@G2}2U{^&@u?Pvti#8O;C-A9BQ1;XhgR z&KfQYzWW}R`xGgJAW!i0?S%UfEAOpYqL2fWkUVXYs}##7@(P|IC|m+CUw5`Zz?@1l zwpj|${JS?IeOX)ISjAmwo=2lAuqMawFfV=WgTXsJJN;KOEN_bxD#G$7d^^W#jJM`$ zWu4K`Zp6*VkGHaz@MCqW%w8~Fo`fh&r`ZP8M+Yr1?7g}%UD?>3>Y=F!2h7#2c#0}R zPuXlhLB6MK$EY|ek$06>LxtwTbo_JSWw3`K%bbjSz+b7 zgH7Q0i<~0Q$$Uzb?uI64#eO0`S}~{c>P)N*MFfZ`lvKPd>0aD8>UMwkYaBrw8m7&1 zy6XLNwMq^@_Eq z!fT}Y7n9eyYJXq3n0bT7mX*IRI~bnZo?N?z=_wkbQ%Bo){IwFjeBU|Vo>4NLz{y9d zT5@-VHKjoUTWZhCgec!KekCLPmZvuEg4_)GgNpH46c)*y)H;tR2tvQ>}x zQqk@%pEi#hs>vS;?v?1O`-JyPP7I9vfP_oWS__MCu$XvA$$a7o!!BPgms`-0CVn)hwZE)F)t1*4`~?g45)`oKVZQx=SFr$fiy z#q*=$X8)r~i5?E8iK&m`RVo~`YDg5g<7SOzx$yPBpa3Q2>H_pH*7AOmTk}$X4>dvu zn5n^nN?!&Vbeo>~(}V8`P^W*|HhXN#d~a$`W`o;}Ilpqp{sX)v-kWTdmVCnTH@%V4 zc|gmb;wln?6gcE(oSS6M9t()_6GYw6@$B@CKtYLk7ro3sKty&A*GYLM6!EEmAP*HXf4 z({h-SrQR}Gt%`Q%lSz3F|8 zCZR>V<|KuR1mtvE;hh(8UO_vv4@Va?w1pTn4>i49E_HIZFPiT19O6NKY#pK}G(U%8EUN-W5{9rTch(X<8I z0RVicspng%Q;TvDAb%vb5_U?Vh1+z^?IRUTD{(qUyhtW&n$G!C$O^+vc;UOIrQXoW7U zP*p;QzXe9cm{=(6Fh?Gzw94I}}e((16|rA@~t*dQ5}fgZ~}ub+YaDV+EKhxbKE} z)6mKg7fvgivr;r#SVI zj2C})!0;5Xj9LVM2^X~}tw&oZ3LN}xXFoZc2#U%PuNau@S*zU$zyH%iD*K9mm-+7{$_LNHU}?gOX=!6WUAdP!4UH zUyNBmXScR1ehSU!!0Pj=&^Erf|9~SoW<-Eljwu;Q1C`CZmW5%Q2x01v;0$8saQuDXq>6#7 zHQHwaq^x`|xc^LAVK3j88J1n11w~QetfVwM;w)^mgnQ&;Ksxttl%4M84R!mTA@=zD^zolRx^1)5+Gn_R#u27)-bzsx&d0MZxF** zmVtRkrXl``Ujp;IzlV}>f^$Jb^I6^zlMkGLxrn1yD>8MSpFae4(6A)m&FIV;yfapE zmlF}c3Y);7o%;6GXy-;dcM)gMw_`B3kvV1vBZhR`FNG9*72178CJRm>Z{d3AjX6q2 zGsba2YuSd|bPcMQ%MswfbLAA>m=*$fF;~kzP+*y-#mfN7K*6!Suw7+UeJ8gW%k_Z} z0u}xs?CD1!zl)~r*|<-!Zn~rj$ETL-n6H3#2Zi#$q3{Z&@`^)fLEp_N3cDKR2Pw7(oN>`?GH2{+ zxU)Z5sMiNREeSYx^UpWCIar*#d=9*u@{g82se&1$f~Oyk1_xg)3V!hrT`tH4lQ9I@= zZ^PtaZy4-QGtC*Lb33N2f=d@tf%pawEHe@TwXi;_Uz(2hlciB|j@Z20L^2OR z)l<(L*LTO@4nxena%kaqGD5$GXaRKc182F;GQUL!Cdd@!l5k0lPjSHDUd-pYhAz*j z&1Ojm#7nT#kEK(9P_QX)y?=+CepYAI%Fy* z;j_8>Ghj@}fV1c|5oRNMOhX{$fH4LotI_M^vymZ17>p?l$|g|$K z2E=I%#yVtEw`8Ep=P((a4h*KOL5Zn(FD6Z-gGg&}A$0 z^=XdrIjA9DKbP-CLSsq>##RB?PNQ5@)4BsrPazpg*o~6j(x5lC9u|;)%e4w(o4xyO z;A|+x)Oin2?L8kBy+JhgOTABJx9>6|m;3%tAbD)pYsC>Im~usjuS$X$2m0Q zv=Wc|%?2`$`6_c>V&v0x{8zXUphnK3lZtscG)n_lst&@fn|_CZ(9woL@DRkFU7Cek zIb65Lpev7zLVFmw+q4nBdtZv956~iMRlgU82*dIoK84QqeJRmnzo%D*VA26`j6mlK zuDGd0#;F(QK0gYV7Kc^QS-Qwwn)Y5d_M{A5B4G*r=zN7P3eQ75a*O@9PAY~0di^^& zCNg>u9kPU;FG(xE=45OFmLLlP8x6ivZvR%AcKr1e4RR=-p1^oE?(9)vSCuQOT6ZRQ zH*Kw>Y=M1wm;%&Mk|Qg%dFEA_p_u-au(P~qI10od`DiU{31beR_TpYtEJfDqXCa6o zjG&o#7_t3c%^Vl{(dSbG z%eL;D!65Z6Y=n~3`wNV`0zwK@NQx-TxkkVo?kflV6RT@sFy@in2p-v;lM3u=| zfc_DOfionN;fzd}FhvM%=y0?=^bU?V`$QbuI^ogyJb|+&6%{AZa++7eX98VM8Js75^)OP@j zxS}=rsJGQf#$6F3V|(}cGH6pEn?|5!i;q_MmPA2!V2&0>?>E8^p!#(UWnx_28)@#( z>geLgGP*yGR*oDgur9P$j_4oAJOtH29`TR&$A391eGaf9;<@3B9?|FoE;Tr3U zy)3ezA%5BjJUCMR5!3~7Sh}|r0PVu^6!b6U@r!Puz%F9I?~Jm;+Ej?XIr!W&e)FVP zC&N1?eH6iM0cJOUe>YNI2V6naTgErSsZG;RN`IiT(Kp_y4@3}^qbLD&A7jdX1A|By z&<`Z{y~mcKlBoJlxq?HQDZx=9xhP}^lYzY9vdUV2>k~ORRKWuqBdLS222=AxwvA{T z!eNHy$F(|K3h5k8?#}M>s9qA)cMgiYxl#VHA_(Pj$!j<_f*yrv4oDi#fMH}X8ln<} zWhDK*)6Px_Sd?(>{OJw!`9@VZE;92^7(x!BUi05fWOnq(<9oyBjYSmx#P1A;k7E!X zbkZkNH7s9421Qz+Md`5p`X9avAb2r!qgSH7>G8|MSB}|M}wo=m-}I_#xuPf0GX37c`vJmC@T-{DO5m zmDVyi@^luN1j4_J5z!V@hhreC$tt?wu^<{Hx>T9&zl0`uH3>6`I2!_`rv^1_GM6Lw8Q32}rv{2;ST%DL37(=?^b0 zUn|oM;GFE&<(7XSv{{CR67^lAuBF@%3Cy%;JvLqyPX*fSx{$fQ;$itvU(WXVm&>-w zoU+m5--=iT$1hmbQrYEM4 z;brp+;elB6NEITWmae*e7Iu6R;u@kt>(j)&a|RAPyoWE)i$ETV;I7TGQ}K>B&UXFn z8kUR&Is(ksy)6-RxP-ddRzvTSv2K~hO$FDsRa5@@SwmYFi^oToqz#qgQelrB$KGLc z2kzJ!)90TI`_k?Ib1ZE5BBUS&bV%kf_J46i^d{Hc0|y?gFPb}*?y=kVn~yJ`DcO?eEg|#y$17j$Z@oN3tBHKo4axWd zUm$NvSVK1|X?c`Mj_nJLSB=Qz$`!#pG_N^z>6{OnOxmqX@zXbH-7o20eU1C9xO2LQhEm0Pw9PMfvQ_r`>M^FUH92PVL!1};G;5ndpCYTiJ_|?g zR*kXsNbtx!O%lL@`aTWFiVoY?By<1Qj81*~q7#QtNE3Ia%xA?UL;4KCKvkv>NN8gT z-+($^{7$haJ*P{Omb%s*m!lw+%l_)5mq%2LT+#(-5t1{bw;VI^5v}^iZA^qh^WR3+(9J19~4(Xz7bKAB_?( z`dtptHx66=!qk5~uAY1QkXUUJu>>xz6v1&;-B7%?DL$TM|oI!FXd|IVWg4{`l$dpW^HO zYC^|v<-Vq_D&;i>iVyR%*%)EWt?ncJD{i%S ztfg$NTK&MiTlegn-x2>z`*n8E$qg*+{CRMn(0W>E&y?fmV(zYz5pBwN)t0h|w!`yc z(l@iem59D-?O?r5^~PCZ^~&qD6C;Un#fRrOe>Qz~41OoCQ#6jA#H>rL7Umena&{qpDPy7hcz?4Rn| zOqIqHY7Yv&ffLl|yYp#6y7x;Hgq06vRFpk}yIb9ZwFzzMa_NEYm#&OyyTT-uWIUBa zveJ4R_5O$YRSbD;vT`oZ`d>-J+a0f0NepCdWXRWNfKf+fZ}%fM9)a}lSv3eH<6GFm z_kx|{uQFQ7)=Tsgk5b*hz1Pn>$X#7Imeu?oMa|F3UI;gK`TnFGb905=#nSg>eyHuU zbNBR^LyzAxTB?TVbJl~y!kWfn_>Q(ttSVzS(Z|hXdNOjC_`RkCb;Gz_-miPfj#x zWVYygdu~^7prI&C@BLI<%y2i-Uu<>_x3)9rGHAPTn1gxvpM3oZO~94zMpa2=YO(FZ z_4B`B=vO&yb+xhyrs2x!T;H**I8`uVz7{Jj?*5FsGEl9b+QsoZ6s*VPUnF{M zGgUmeNoC24D(|)-~?aFvklFCw&^;@)yzrAH^ zvbW13q*Qua7Oz>WvYRz#jL_RHlo-t*E})qK$0u`jNs?=!H`=?ETE({8C~Cl8Ckw96 zi4RGw4_9`hTMJ=MNk))8QRx2{Amz;2Sv);)gn9H`SC_Y;RGQ_Z^SbE{9$u|2vt7l! z1%bC)x>2tW?-d8jPQYT^e+*P&OTYHs86Ec$umwj|x*plOGOLy%lZ&LU4M**JK1+Dd zOgrv+B8N6Ke*d}MN?j5;mhH#B{sG z`9p?Ey9u;I?`|+@T@{jyW{9p{dRy}jqhwAsu0PkNVEO9O1%ZhpHx8`D&X@jRWh5N- zjdt5L^vT0g>adxvQ=^8koo?oHtMBUVBn?&`xy!RT2@OX~BBS498;Y)5Qa#kBy@j7} zGq^S+X{U4ai}u#_ae=+X8h-76hgNFdH>~5gX);kC3LS*v31r zJ@mAby`5q6ekRIznGAF9tIV6&2$oZhyAnSr$(-q>n|#Di;9C{~0=uPqxCo^d>sY5? z%ku(yCoCxnVX7=|xpj68L|gz^)ASJ(IEa(6YOmZb8UiCXy3*rL!$5Qo{9*o5HT-&Oj&Y)l1wj^U%GJCP%N| z+ZOyY;@b4rXhxm7<5Srx3?NW?u=}b+u0(|_NEG8hrE>266T}b&zH0~ zxO&v^7iB1eY%3V=iG-g1BRdGuR1eMo!c;^Z9DsnY-}K{&>JA zf|HcwFRQ=lPa&#~cX0AB*Gjzq{*PxGgPEU%I=6g-9~?ZMtI;Js-&XgVvFloWsDu`xFS=|Li0E?jS(?$L*hv>K7`3G~=3pB4 zrkx%=|9MsTksU`xd*D-z^uUtVxb%HxmJudr3y*u;rVzhS8-kh23eCK6FYd()dMCa&h z?ib%88s>e~J@wvu8{d8n;u$csdsSCJt8olR7RcO(cUR;yKQ7zU5FHln;1`uko_M#d zSd;zd=l6Ew$u~g`H{HTK9a0NPuWmG?EqVI86_XZxj2Cgd*u)s&dt&F<&MLf(O){$X9>db z0=7jd_v=nnSHW8HRPKb@!PJiiU&Y!jD5Lz=nY_qnPYe}2v6AsJXZ&_rvXvM%^+GN@ zYxHdw&+@Omm9_)8Sx4a!Ao#T*)rq6+&te!ein^zn4^y~4m`T`$8@m>L@@uJhj$HTv z46xm_FXy`Ly+tdaA1_%Pz#R30zs9>yk7h_S@*MSIU8%yp{VkCqdL$2lO#d_Ye{2rF zZ7@wMPFf@y2ei{p2KIbTZZY6C-A8@RQgG#&^tBM_Hxk~Zc_XhwzrAqI>Qp#mcT?lt ztA61t=HWNa6e$mMPsb-@)UUM2e&U#vY}a9X`;-v2ipI>u|tu3Ft9=&>AQ?*r<_Lfr!Hrnps{jOq# z;2Sv8#7DeFAYp|@)B5hg&iQe(`}aPc6ZtXXpq141L8T$H|NIdLM;gUmZ=M?e_qBVp zi*s4bHhbg;zr|AWo#;{4Y5!@^_UlCAtt(?Ar7sFLcWJM=o(e3oMD(vy6?RuToe@7n zy$G$&_?e988GXqu(c)FB2SurO$36yBrABftU{eOG)TVjan!Y{6*6b#|{P2pi*u|-& z*Z$Qnw&re1KHrXRhIhW_@7=$}uj?)m!i*3?cHOGNU2{6zSLIr?bWn1tv+uYjkFn>W z?|z~qha2`@t&^e~`uu(J_uD4Z%yOAJX=)S3G0`LIr{jILD63m+)|aokMdz&_tReQD z)QlcA@#JUqdXmw1+HNDfXh_+NYEbL(@NV;y}W*H;w5!LpdV3i23*0RYx;M8ne0k#FaWeMEZ3*Fx zNT$k}7S@Tuf~W;6tRlx(+wt+7CH7iny{oG?9Guf$7hN>VqxzBHt2I`}r=l)PBhna| z{*+=fUEf-#(PDG!dkMFX7Fc7FOc_VL;im0OU8MFfug($OpURc}!qs-{Mg=`TM%q23 zXFGZ|M%AZH+5gZ+XVVm{JLXSxzLFcg+w-;H``Ef41KZX0sX^=Z`4EhU<7O4!W_)}R z@7Mq85$33N`7pPR9!Qe)mhAD>5$B+LaF%@L4vYM>$@!YVcMZ&b!l$}|7>bzke^41Z z>s=`QB-;Mc5%f>)fMon|FX6MMGs7<6ifpG-UbuZ<>+pCi zTl3?TwK(F1P1!}W%a%$(*ss1(H@pz1X}{g$A@zoOX6#LcP_8-qUi*xIv9bLDU`uj@rq^qmW(*`Pe zlqzy=xtd$`3Ah;1F0i#MTZ{dcm3{Z(?30h;y0Jwm`4da=mP-af4~nUwEnsABVCmze zdpOTy@}UvoVK~RyiVo|8Lpc;7xzZ9#tIOaDOT~UoVZ;4vi;M2}C3T&Y&#$i>c^q$1 z!zS19VxVzo-KkxXa_qJKx;3`I=^;DHoy>7H zQCp+`{NsCj$=GtJmhTb(bS~J;nNPxgq?xLZ$5bZIX|D~i%cu8+JkpW&Qoh|t&42>U#N>y&gSt% zbaOPGx|I8!_rabqEx!HICN9m=)h!_r%>vht=1fL{1(3T84^U|0Og(IFFY|n!EDG98 z`f&YS9ziu7dt{Q2$33>C_-;as^WZqc`Qqm90X#pQJq-B|Z_B*>l4Z z3~!2Nj{o+~I&SqrEw%h)bu16R33XJYeTdedQ(bE5vual)PTh`YyINDi_+WnXEcK6H zg@%hhZ7PXvZgWZg91>?h!Cz`8iwrk1)Bge2D>80(Jn?5o2qHavozLio8r#Gfrs|gO zGYXgHHklPOMaHEq&m5e8BMad+KzGA*?Y2i!E*eu5YmhA~ChwB*b2mqqk3X$S!LE$B z3M5+?KOSi@;D5EI+2wY`nLc?ytZL#oHNAQgo3I z{M0gtp=)`+ybfMk$Ldy63Nx~PVz8l!KzP~h_#pFfK zikJK86sN*-Dg|h`e+tnkLE1uW-Uv}?ksC*~jLYnmewpAW-kU#hRd+H%C=B?gtm3AY z&Lw6EFF1Cr^|u)AvYlK(ZMC&u*&tGw!;otpCd?+WL< z9#Q5(8II&k_Tt?5-$KV5q=eT@*&P(SRODRWKE33Xji-{E+q7>#c)7G>>)Gd0Z@#9T zxBCyO2Yxg>?S0R1VzY+syNnC-zWfH(a;s+W9?Z*CIPk1@W73PSO^+QUymx2rkDGPO zaKW*C8BIlED>RS9Yqorii1*$7_f8?OB>Nu9^($a$QQ_3T#Y?x$Izu;z7^?TsJ1+jISj-8XknUU;mt zf6eI|@y9NGo4@~cS@Gm8mIuA&%O-w5o-4Ftb%#XugiqI6?qxE3NDufEbNv6A=US#~ zYh^YZ5qM^}Auu4~@+rcPkA^{Re&rJ5pH=^A?O^Sxa0zTNLv zx12vza_r{0V+_*E{@sq9efgrzjIU919hRCNyJUC^n63F4y?_0Ca$i#G$~^n^lg%@X7=)bZQt(T z?9c7r{vD7>-;TQ`XcuA;6g` z28V+nW71bFic-}V1szEjumBRh%EvaG_~slOC3{{kWOWK~^AJ$C(;A@eHiLe{7`Y!KL&QiXkuC!JJ=HF0FdK-)Y4qdC(jIgO971_Cl}yP?M%O z{SkFO@h@$`ltVMawZ8iw1u6NW@q2dTwot9(o0kTpGH8J#p(iK`805uvy9?f=J3t(F zv1Z$zyLUp*);BmT)qn^^d#wg~WAXodu8-lscI?6}Pl3xdfFa4bZGEC2uy*{hT_Kg{ z(f@zZ86amiut8k00C?~PNR>0#kcKwk{4)^ng5#1w3b=L;2u#4i%#ZmdKI;Vst0I#4sF8}}l diff --git a/src/configng/functions/bash-utility-master/src/array.sh b/src/configng/functions/bash-utility-master/src/array.sh deleted file mode 100644 index 42d5883b8..000000000 --- a/src/configng/functions/bash-utility-master/src/array.sh +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env bash - -# @file Array -# @brief Functions for array operations and manipulations. - -# @description Check if item exists in the given array. -# -# @example -# array=("a" "b" "c") -# array::contains "c" ${array[@]} -# #Output -# 0 -# -# @arg $1 mixed Item to search (needle). -# @arg $2 array array to be searched (haystack). -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found in the array. -# @exitcode 2 Function missing arguments. -array::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare query="${1:-}" - shift - - for element in "${@}"; do - [[ "${element}" == "${query}" ]] && return 0 - done - - return 1 -} - -# @description Remove duplicate items from the array. -# -# @example -# array=("a" "b" "a" "c") -# printf "%s" "$(array::dedupe ${array[@]})" -# #Output -# a -# b -# c -# -# @arg $1 array Array to be deduped. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Deduplicated array. -array::dedupe() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -A arr_tmp - declare -a arr_unique - for i in "$@"; do - { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue - arr_unique+=("${i}") && arr_tmp[${i}]=x - done - printf '%s\n' "${arr_unique[@]}" -} - -# @description Check if a given array is empty. -# -# @example -# array=("a" "b" "c" "d") -# array::is_empty "${array[@]}" -# -# @arg $1 array Array to be checked. -# -# @exitcode 0 If the given array is empty. -# @exitcode 2 If the given array is not empty. -array::is_empty() { - declare -a array - local array=("$@") - if [ ${#array[@]} -eq 0 ]; then - return 0 - else - return 1 - fi -} -# @description Join array elements with a string. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s" "$(array::join "," "${array[@]}")" -# #Output -# a,b,c,d -# printf "%s" "$(array::join "" "${array[@]}")" -# #Output -# abcd -# -# @arg $1 string String to join the array elements (glue). -# @arg $2 array array to be joined with glue string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. -array::join() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare delimiter="${1}" - shift - printf "%s" "${1}" - shift - printf "%s" "${@/#/${delimiter}}" -} - -# @description Return an array with elements in reverse order. -# -# @example -# array=(1 2 3 4 5) -# printf "%s" "$(array::reverse "${array[@]}")" -# #Output -# 5 4 3 2 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The reversed array. -array::reverse() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare min=0 - declare -a array - array=("$@") - declare max=$((${#array[@]} - 1)) - - while [[ $min -lt $max ]]; do - # Swap current first and last elements - x="${array[$min]}" - array[$min]="${array[$max]}" - array[$max]="$x" - - # Move closer - ((min++, max--)) - done - printf '%s\n' "${array[@]}" -} - -# @description Returns a random item from the array. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s\n" "$(array::random_element "${array[@]}")" -# #Output -# c -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Random item out of the array. -array::random_element() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array - local array=("$@") - printf '%s\n' "${array[RANDOM % $#]}" -} - -# @description Sort an array from lowest to highest. -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# 1 -# 2 -# 4 5 -# a -# a c -# d -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout sorted array. -array::sort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort <<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Sort an array in reverse order (highest to lowest). -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# d -# a c -# a -# 4 5 -# 2 -# 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout reverse sorted array. -array::rsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort -r<<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Bubble sort an integer array from lowest to highest. -# This sort does not work on string array. -# @example -# iarr=(4 5 1 3) -# array::bsort "${iarr[@]}" -# #Output -# 1 -# 3 -# 4 -# 5 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout bubble sorted array. -array::bsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare tmp - declare arr=("$@") - for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do - for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do - if [[ ${arr[i]} -gt ${arr[j]} ]]; then - # echo $i $j ${arr[i]} ${arr[j]} - tmp=${arr[i]} - arr[i]=${arr[j]} - arr[j]=$tmp - fi - done - done - printf "%s\n" "${arr[@]}" -} - -# @description Merge two arrays. -# Pass the variable name of the array instead of value of the variable. -# @example -# a=("a" "c") -# b=("d" "c") -# array::merge "a[@]" "b[@]" -# #Output -# a -# c -# d -# c -# -# @arg $1 string variable name of first array. -# @arg $2 string variable name of second array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Merged array. -array::merge() { - [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr1=("${!1}") - declare -a arr2=("${!2}") - declare out=("${arr1[@]}" "${arr2[@]}") - printf "%s\n" "${out[@]}" -} diff --git a/src/configng/functions/bash-utility-master/src/check.sh b/src/configng/functions/bash-utility-master/src/check.sh deleted file mode 100644 index 2b7c1eb1d..000000000 --- a/src/configng/functions/bash-utility-master/src/check.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# @file Check -# @brief Helper functions. - -# @description Check if the command exists in the system. -# -# @example -# check::command_exists "tput" -# -# @arg $1 string Command name to be searched. -# -# @exitcode 0 If the command exists. -# @exitcode 1 If the command does not exist. -# @exitcode 2 Function missing arguments. -check::command_exists() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - hash "${1}" 2> /dev/null -} - -# @description Check if the script is executed with sudo privilege. -# -# @example -# check::is_sudo -# -# @noargs -# -# @exitcode 0 If the script is executed with root privilege. -# @exitcode 1 If the script is not executed with root privilege -check::is_sudo() { - if [[ $(id -u) -ne 0 ]]; then - return 1 - fi -} diff --git a/src/configng/functions/bash-utility-master/src/collection.sh b/src/configng/functions/bash-utility-master/src/collection.sh deleted file mode 100644 index 9f0e244a2..000000000 --- a/src/configng/functions/bash-utility-master/src/collection.sh +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env bash - -# @file Collection -# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -# @description Iterates over elements of collection and invokes iteratee for each element. -# Input to the function can be a pipe output, here-string or file. -# @example -# test_func(){ -# printf "print value: %s\n" "$1" -# return 0 -# } -# arr1=("a b" "c d" "a" "d") -# printf "%s\n" "${arr1[@]}" | collection::each "test_func" -# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -# #output -# print value: a b -# print value: c d -# print value: a -# print value: d -# -# @example -# # If other function from this library is already used to process the array. -# # Then following method could be used to pass the array to the function. -# out=("$(array::dedupe "${arr1[@]}")") -# collection::each "test_func" <<< "${out[@]}" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output of iteratee function. -collection::each() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - done -} - -# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "4") -# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If iteratee function fails. -# @exitcode 2 Function missing arguments. -collection::every() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return 1 - fi - - done -} - -# @description Iterates over elements of array, returning all elements where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -# #output -# 1 -# 2 -# 3 -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values matching the iteratee function. -collection::filter() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s\n" "${it}" - fi - done -} - -# @description Iterates over elements of collection, returning the first element where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("1" "2" "3" "a") -# check_a(){ -# [[ "$1" = "a" ]] -# } -# printf "%s\n" "${arr[@]}" | collection::find "check_a" -# #Output -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -# -# @stdout first array value matching the iteratee function. -collection::find() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s" "${it}" - return 0 - fi - done - - return 1 -} - -# @description Invokes the iteratee with each element passed as argument to the iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# opt=("-a" "-l") -# printf "%s\n" "${opt[@]}" | collection::invoke "ls" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output from the iteratee function. -collection::invoke() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a args=() - declare func="${1}" - while read -r it; do - args=("${args[@]}" "$it") - done - - eval "${func}" "${args[@]}" -} - -# @description Creates an array of values by running each element in array through iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3") -# add_one(){ -# i=${1} -# i=$(( i + 1 )) -# printf "%s\n" "$i" -# } -# printf "%s\n" "${arri[@]}" | collection::map "add_one" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output result of iteratee on value. -collection::map() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - declare out - - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - out="$("${func}")" - else - out="$("${func}" "$it")" - fi - - declare -i ret=$? - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - printf "%s\n" "${out}" - done -} - -# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -# #Ouput -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values not matching the iteratee function. -# @see collection::filter -collection::reject() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - echo "$it" - fi - - done -} - -# @description Checks if iteratee returns true for any element of the array. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("a" "b" "3" "a") -# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If match successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -collection::some() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - - declare -i ret=$? - - if [[ $ret -eq 0 ]]; then - return 0 - fi - done - - return 1 -} diff --git a/src/configng/functions/bash-utility-master/src/date.sh b/src/configng/functions/bash-utility-master/src/date.sh deleted file mode 100644 index 41f071792..000000000 --- a/src/configng/functions/bash-utility-master/src/date.sh +++ /dev/null @@ -1,744 +0,0 @@ -#!/usr/bin/env bash - -# @file Date -# @brief Functions for manipulating dates. - -# @description Get current time in unix timestamp. -# -# @example -# echo "$(date::now)" -# #Output -# 1591554426 -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout current timestamp. -date::now() { - declare now - now="$(date --universal +%s)" || return $? - printf "%s" "${now}" -} - -# @description convert datetime string to unix timestamp. -# -# @example -# echo "$(date::epoc "2020-07-07 18:38")" -# #Output -# 1594143480 -# -# @arg $1 string date time in any format. -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp for specified datetime. -date::epoc() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare date - date=$(date -d "${1}" +"%s") || return $? - printf "%s" "${date}" -} - -# @description Add number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days_from "1594143480")" -# #Output -# 1594229880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months_from "1594143480")" -# #Output -# 1596821880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years_from "1594143480")" -# #Output -# 1625679480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::add_weeks_from "1594143480")" -# #Output -# 1594748280 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours_from "1594143480")" -# #Output -# 1594147080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes_from "1594143480")" -# #Output -# 1594143540 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds_from "1594143480")" -# #Output -# 1594143481 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days "1")" -# #Output -# 1591640826 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months "1")" -# #Output -# 1594146426 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years "1")" -# #Output -# 1623090426 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_weeks "1")" -# #Output -# 1592159226 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours "1")" -# #Output -# 1591558026 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes "1")" -# #Output -# 1591554486 -# -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds "1")" -# #Output -# 1591554427 -# -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days_from "1594143480")" -# #Output -# 1594057080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months_from "1594143480")" -# #Output -# 1591551480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years_from "1594143480")" -# #Output -# 1562521080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks_from "1594143480")" -# #Output -# 1593538680 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours_from "1594143480")" -# #Output -# 1594139880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes_from "1594143480")" -# #Output -# 1594143420 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds_from "1594143480")" -# #Output -# 1594143479 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days "1")" -# #Output -# 1588876026 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months "1")" -# #Output -# 1559932026 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years "1")" -# #Output -# 1591468026 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks "1")" -# #Output -# 1590949626 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours "1")" -# #Output -# 1591550826 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes "1")" -# #Output -# 1591554366 -# -# @arg $1 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds "1")" -# #Output -# 1591554425 -# -# @arg $1 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Format unix timestamp to human readable format. -# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. -# -# @example -# echo echo "$(date::format "1594143480")" -# #Output -# 2020-07-07 18:38:00 -# -# @arg $1 int unix timestamp. -# @arg $2 string format control characters based on `date` command (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate time string. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -date::format() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp format out - timestamp="${1}" - format="${2:-"%F %T"}" - out="$(date -d "@${timestamp}" +"${format}")" || return $? - printf "%s" "${out}" - -} diff --git a/src/configng/functions/bash-utility-master/src/debug.sh b/src/configng/functions/bash-utility-master/src/debug.sh deleted file mode 100644 index 82828734f..000000000 --- a/src/configng/functions/bash-utility-master/src/debug.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash - -# @file Debug -# @brief Functions to facilitate debugging scripts. - -# @description Prints the content of array as key value pair for easier debugging. -# Pass the variable name of the array instead of value of the variable. -# @example -# array=(foo bar baz) -# printf "Array\n" -# printarr "array" -# declare -A assoc_array -# assoc_array=([foo]=bar [baz]=foobar) -# printf "Assoc Array\n" -# printarr "assoc_array" -# #Output -# Array -# 0 = foo -# 1 = bar -# 2 = baz -# Assoc Array -# baz = foobar -# foo = bar -# -# @arg $1 string variable name of the array. -# -# @stdout Formatted key value of array. -debug::print_array() { - declare -n __arr="$1" - for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done -} - -# @description Function to print ansi escape sequence as is. -# This function helps debug ansi escape sequence in text by displaying the escape codes. -# -# @example -# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -# debug::print_ansi "$txt" -# #Output -# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -# -# @arg $1 string input with ansi escape sequence. -# -# @stdout Ansi escape sequence printed in output as is. -debug::print_ansi() { - #echo $(tr -dc '[:print:]'<<<$1) - printf "%s\n" "${1//$'\e'/\\e}" - -} diff --git a/src/configng/functions/bash-utility-master/src/file.sh b/src/configng/functions/bash-utility-master/src/file.sh deleted file mode 100644 index 71afd8476..000000000 --- a/src/configng/functions/bash-utility-master/src/file.sh +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env bash - -# @file File -# @brief Functions for handling files. - -# @description Create temporary file. -# Function creates temporary file with random name. The temporary file will be deleted when script finishes. -# -# @example -# echo "$(file::make_temp_file)" -# #Output -# tmp.vgftzy -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp file. -# -# @stdout file name of temporary file created. -file::make_temp_file() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -# @description Create temporary directory. -# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. -# -# @example -# echo "$(utility::make_temp_dir)" -# #Output -# tmp.rtfsxy -# -# @arg $1 string Temporary directory prefix -# @arg $2 string Flag to auto remove directory on exit trap (true) -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp directory. -# @exitcode 2 Missing arguments. -# -# @stdout directory name of temporary directory created. -file::make_temp_dir() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare temp_dir prefix="${1}" trap_rm="${2}" - temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") - if [[ -n "${trap_rm}" ]]; then - trap 'rm -rf "${temp_dir}"' EXIT - fi - printf "%s" "${temp_dir}" -} - -# @description Get only the filename from string path. -# -# @example -# echo "$(file::name "/path/to/test.md")" -# #Output -# test.md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout name of the file with extension. -file::name() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf "%s" "${1##*/}" -} - -# @description Get the basename of file from file name. -# -# @example -# echo "$(file::basename "/path/to/test.md")" -# #Output -# test -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout basename of the file. -file::basename() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare file basename - file="${1##*/}" - basename="${file%.*}" - - printf "%s" "${basename}" -} - -# @description Get the extension of file from file name. -# -# @example -# echo "$(file::extension "/path/to/test.md")" -# #Output -# md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 1 If no extension is found in the filename. -# @exitcode 2 Function missing arguments. -# -# @stdout extension of the file. -file::extension() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare file extension - file="${1##*/}" - extension="${file##*.}" - [[ "${file}" = "${extension}" ]] && return 1 - - printf "%s" "${extension}" -} - -# @description Get directory name from file path. -# -# @example -# echo "$(file::dirname "/path/to/test.md")" -# #Output -# /path/to -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout directory path. -file::dirname() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare tmp=${1:-.} - - [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } - tmp="${tmp%%"${tmp##*[!/]}"}" - - [[ ${tmp} != */* ]] && { printf '.\n' && return; } - tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" - - printf '%s' "${tmp:-/}" -} - -# @description Get absolute path of file or directory. -# -# @example -# file::full_path "../path/to/file.md" -# #Output -# /home/labbots/docs/path/to/file.md -# -# @arg $1 string relative or absolute path to file/direcotry. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# -# @stdout Absolute path to file/directory. -file::full_path() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare input="${1}" - if [[ -f ${input} ]]; then - printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" - elif [[ -d ${input} ]]; then - printf "%s\n" "$(cd "${input}" && pwd)" - else - return 1 - fi -} - -# @description Get mime type of provided input. -# -# @example -# file::mime_type "../src/file.sh" -# #Output -# application/x-shellscript -# -# @arg $1 string relative or absolute path to file/directory. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# @exitcode 3 If file or mimetype command not found in system. -# -# @stdout mime type of file/directory. -file::mime_type() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare mime_type - if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then - if type -p mimetype &> /dev/null; then - mime_type=$(mimetype --output-format %m "${1}") - elif type -p file &> /dev/null; then - mime_type=$(file --brief --mime-type "${1}") - else - return 3 - fi - else - return 1 - fi - printf "%s" "${mime_type}" -} - -# @description Search if a given pattern is found in file. -# -# @example -# file::contains_text "./file.sh" "^[ @[:alpha:]]*" -# file::contains_text "./file.sh" "@file" -# #Output -# 0 -# -# @arg $1 string relative or absolute path to file/directory. -# @arg $2 string search key or regular expression. -# -# @exitcode 0 If given search parameter is found in file. -# @exitcode 1 If search paramter not found in file. -# @exitcode 2 Function missing arguments. -file::contains_text() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - declare -r file="$1" - declare -r text="$2" - grep -q "$text" "$file" -} diff --git a/src/configng/functions/bash-utility-master/src/format.sh b/src/configng/functions/bash-utility-master/src/format.sh deleted file mode 100644 index 7dceb1d59..000000000 --- a/src/configng/functions/bash-utility-master/src/format.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash - -# @file Format -# @brief Functions to format provided input. - -# @internal -# @description Initialisation script when the code is sourced. -# -# @noargs -__init(){ -_check_terminal_window_size -} - -# @internal -# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. -# -# @noargs -_check_terminal_window_size() { - shopt -s checkwinsize && (: && :) - trap 'shopt -s checkwinsize; (:;:)' SIGWINCH -} -# @description Format seconds to human readable format. -# -# @example -# echo "$(format::human_readable_seconds "356786")" -# #Output -# 4 days 3 hours 6 minute(s) and 26 seconds -# -# @arg $1 int number of seconds. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -format::human_readable_seconds() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare T="${1}" - declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" - [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" - [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" - [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" - [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' - printf '%d seconds\n' "${SEC}" -} - -# @description Format bytes to human readable format. -# -# @example -# echo "$(format::bytes_to_human "2250")" -# #Output -# 2.19 KB -# -# @arg $1 int size in bytes. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted file size string. -format::bytes_to_human() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) - while ((b > 1024)); do - d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" - b=$((b / 1024)) && ((s++)) - done - printf "%s\n" "${b}${d} ${S[${s}]}" -} - -# @description Remove Ansi escape sequences from given text. -# -# @example -# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -# #Output -# This is bold red text.This is green text. -# -# @arg $1 string Input text to be ansi stripped. -# -# @exitcode 0 If successful. -# -# @stdout Ansi stripped text. -format::strip_ansi() { - declare tmp esc tpa re - tmp="${1}" - esc=$(printf "\x1b") - tpa=$(printf "\x28") - re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" - while [[ "${tmp}" =~ $re ]]; do - tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" - done - printf "%s" "${tmp}" -} - -# @description Prints the given text to centre of terminal. -# -# @example -# format::text_center "This text is in centre of the terminal." "-" -# -# @arg $1 string Text to be printed. -# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::text_center() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare input="${1}" symbol="${2:- }" filler out no_ansi_out - no_ansi_out=$(format::strip_ansi "$input") - declare -i str_len=${#no_ansi_out} - declare -i filler_len="$(((COLUMNS - str_len) / 2))" - - [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" - for ((i = 0; i < filler_len; i++)); do - filler+="${symbol}" - done - - out="${filler}${input}${filler}" - [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" - printf "%s" "${out}" -} - -# @description Format String to print beautiful report. -# -# @example -# format::report "Initialising mission state" "Success" -# #Output -# Initialising mission state ....................................................................[ Success ] -# -# @arg $1 string Text to be printed on the left. -# @arg $2 string Text to be printed within the square brackets. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::report() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare symbol="." to_print y hl hlout out - declare input1="${1} " input2="${2}" - input2="[ $input2 ]" - to_print="$((COLUMNS * 60 / 100))" - y=$(( to_print - ( ${#input1} + ${#input2} ) )) - hl="$(printf '%*s' $y '')" - hlout=${hl// /${symbol}} - out="${input1}${hlout}${input2}" - printf "%s\n" "${out}" -} - -# @description Trim given text to width of the terminal window. -# -# @example -# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -# #Output -# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -# -# @arg $1 string Text of first sentence. -# @arg $2 string Text of second sentence (optional). -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout trimmed text. -format::trim_text_to_term() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare to_print out input1="$1" input2="$2" - if [[ $# = 1 ]]; then - to_print="$((COLUMNS * 93 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } - else - to_print="$((COLUMNS * 40 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } - to_print="$((COLUMNS * 53 / 100))" - { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } - fi - printf "%s" "$out" -} - -__init diff --git a/src/configng/functions/bash-utility-master/src/interaction.sh b/src/configng/functions/bash-utility-master/src/interaction.sh deleted file mode 100644 index 910b60e1c..000000000 --- a/src/configng/functions/bash-utility-master/src/interaction.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# @file Interaction -# @brief Functions to enable interaction with the user. - -# @description Prompt yes or no question to the user. -# -# @example -# interaction::prompt_yes_no "Are you sure to proceed" "yes" -# #Output -# Are you sure to proceed (y/n)? [y] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer \[yes/no\] (optional). -# -# @exitcode 0 If user responds with yes. -# @exitcode 1 If user responds with no. -# @exitcode 2 Function missing arguments. -interaction::prompt_yes_no() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare def_arg response - def_arg="" - response="" - - case "${2}" in - [yY] | [yY][eE][sS]) - def_arg=y - ;; - [nN] | [nN][oO]) - def_arg=n - ;; - esac - - while :; do - printf "%s (y/n)? " "${1}" - [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -z "${response}" ]] && response="${def_arg}" - - case "${response}" in - [yY] | [yY][eE][sS]) - response=y - break - ;; - [nN] | [nN][oO]) - response=n - break - ;; - *) - response="" - ;; - esac - done - - [[ "${response}" = 'y' ]] && return 0 || return 1 -} - -# @description Prompt question to the user. -# -# @example -# interaction::prompt_response "Choose directory to install" "/home/path" -# #Output -# Choose directory to install? [/home/path] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer (optional). -# -# @exitcode 0 If user responds with answer. -# @exitcode 2 Function missing arguments. -# -# @stdout User entered answer to the question. -interaction::prompt_response() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare def_arg response - response="" - def_arg="${2}" - - while :; do - printf "%s ? " "${1}" - [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -n "${response}" ]] && break - - if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then - response="${def_arg}" - break - fi - done - - [[ "${response}" = "-" ]] && response="" - - printf "%s" "${response}" -} diff --git a/src/configng/functions/bash-utility-master/src/json.sh b/src/configng/functions/bash-utility-master/src/json.sh deleted file mode 100644 index 73476618e..000000000 --- a/src/configng/functions/bash-utility-master/src/json.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# @file Json -# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. - -# @description Extract value from json based on key and position. -# Input to the function can be a pipe output, here-string or file. -# @example -# json::get_value "id" "1" < json_file -# json::get_value "id" <<< "${json_var}" -# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -# -# @arg $1 string id of the field to fetch. -# @arg $2 int position of value to extract.Defaults to 1.(optional) -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string value of extracted key. -json::get_value() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare LC_ALL=C num="${2:-1}" - grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p -} diff --git a/src/configng/functions/bash-utility-master/src/misc.sh b/src/configng/functions/bash-utility-master/src/misc.sh deleted file mode 100644 index fca9a75bf..000000000 --- a/src/configng/functions/bash-utility-master/src/misc.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash - -# @file Miscellaneous -# @brief Set of miscellaneous helper functions. - -# @internal -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -_is_terminal() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Check if internet connection is available. -# -# @example -# misc::check_internet_connection -# -# @noargs -# -# @exitcode 0 If script can connect to internet. -# @exitcode 1 If script cannot access internet. -misc::check_internet_connection() { - declare check_internet - if _is_terminal; then - check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" - else - check_internet="$(curl --compressed -Is google.com -m 10)" - fi - if [[ -z ${check_internet} ]]; then - return 1 - fi -} - -# @description Get list of process ids based on process name. -# -# @example -# misc::get_pid "chrome" -# #Ouput -# 25951 -# 26043 -# 26528 -# 26561 -# -# @arg $1 Name of the process to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout list of process ids. -misc::get_pid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - pgrep "${1}" -} - -# @description Get user id based on username. -# -# @example -# misc::get_uid "labbots" -# #Ouput -# 1000 -# -# @arg $1 username to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string uid for the username. -misc::get_uid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - user_id=$(id "${1}" 2> /dev/null) - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - printf "No user found with username: %s" "${1}\n" - return 1 - fi - - printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' - - unset user_id -} - -# @description Generate random uuid. -# -# @example -# misc::generate_uuid -# #Ouput -# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -# -# @noargs -# -# @exitcode 0 If match successful. -# -# @stdout random generated uuid. -misc::generate_uuid() { - C="89ab" - - for ((N=0;N<16;++N)); do - B="$((RANDOM%256))" - - case "$N" in - 6) printf '4%x' "$((B%16))" ;; - 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; - - 3|5|7|9) - printf '%02x-' "$B" - ;; - - *) - printf '%02x' "$B" - ;; - esac - done - - printf '\n' -} diff --git a/src/configng/functions/bash-utility-master/src/os.sh b/src/configng/functions/bash-utility-master/src/os.sh deleted file mode 100644 index 78e0d36c5..000000000 --- a/src/configng/functions/bash-utility-master/src/os.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env bash - -# @file Operating System -# @brief Functions to detect Operating system and version. - -# @description Identify the OS the function is run on. -# -# @noargs -# -# @example -# os::detect_os -# #Output -# linux -# -# @exitcode 0 If OS is successfully detected. -# @exitcode 1 If unable to detect OS. -# -# @stdout Operating system name (linux, mac or windows). -os::detect_os() { - declare uname os - uname=$(command -v uname) - - case $("${uname}" | tr '[:upper:]' '[:lower:]') in - linux*) - os="linux" - ;; - darwin*) - os="mac" - ;; - msys* | cygwin* | mingw* | nt | win*) - # or possible 'bash on windows' - os="windows" - ;; - *) - return 1 - ;; - esac - printf "%s" "${os}" -} - -# @description Identify the distribution flavour of linux. -# -# @noargs -# -# @example -# os::detect_linux_distro -# #Output -# ubuntu -# @exitcode 0 If Linux distro is successfully detected. -# @exitcode 1 If unable to detect OS distro. -# -# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). -os::detect_linux_distro() { - declare distro - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro="${NAME}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro=$(lsb_release -si) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro="${DISTRIB_ID}" - elif [[ -f /etc/debian_version ]]; then - # Older Debian/Ubuntu/etc. - distro="debian" - elif [[ -f /etc/SuSe-release ]]; then - # Older SuSE/etc. - distro="suse" - elif [[ -f /etc/redhat-release ]]; then - # Older Red Hat, CentOS, etc. - distro="redhat" - else - return 1 - fi - printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' -} - -# @description Identify the Linux version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 20.04 -# -# @exitcode 0 If Linux version is successfully detected. -# @exitcode 1 If unable to detect Linux version. -# -# @stdout Linux OS version number (18.04, 20.04, etc.,). -os::detect_linux_version() { - declare distro_version - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro_version="${VERSION_ID}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro_version=$(lsb_release -sr) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro_version="${DISTRIB_RELEASE}" - else - return 1 - fi - printf "%s" "${distro_version}" -} - -# @description Identify the Linux codename. -# -# @noargs -# -# @example -# os::detect_linux_codename -# #Output -# jammy -# -# @exitcode 0 If Linux codename is successfully detected. -# @exitcode 1 If unable to detect Linux codename. -# -# @stdout Linux OS codename number (buster, jammy, etc.,). -os::detect_linux_codename() { - declare distro_codename - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro_codename="${VERSION_CODENAME}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro_codename=$(lsb_release -cs) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro_codename="${DISTRIB_CODENAME}" - else - return 1 - fi - printf "%s" "${distro_codename}" -} - -# @description Identify the MacOS version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 10.15.7 -# @exitcode 0 If MacOS version is successfully detected. -# @exitcode 1 If unable to detect MacOS version. -# -# @stdout MacOS version number (10.15.6, etc.,) -os::detect_mac_version() { - if [[ "$(os::detect_os)" = "mac" ]]; then - declare mac_version - mac_version="$(sw_vers -productVersion)" - printf "%s" "${mac_version}" - else - return 1 - fi -} diff --git a/src/configng/functions/bash-utility-master/src/string.sh b/src/configng/functions/bash-utility-master/src/string.sh deleted file mode 100644 index aa522cb55..000000000 --- a/src/configng/functions/bash-utility-master/src/string.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env bash - -# @file String -# @brief Functions for string operations and manipulations. - -# @description Strip whitespace from the beginning and end of a string. -# -# @example -# echo "$(string::trim " Hello World! ")" -# #Output -# Hello World! -# -# @arg $1 string The string to be trimmed. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The trimmed string. -string::trim() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - : "${1#"${1%%[![:space:]]*}"}" - : "${_%"${_##*[![:space:]]}"}" - printf '%s\n' "$_" -} - -# @description Split a string to array by a delimiter. -# -# @example -# array=( $(string::split "a,b,c" ",") ) -# printf "%s" "$(string::split "Hello!World" "!")" -# #Output -# Hello -# World -# -# @arg $1 string The input string. -# @arg $2 string The delimiter string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. -string::split() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr=() - IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" - printf '%s\n' "${arr[@]}" -} - -# @description Strip characters from the beginning of a string. -# -# @example -# echo "$(string::lstrip "Hello World!" "He")" -# #Output -# llo World! -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::lstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1##$2}" -} - -# @description Strip characters from the end of a string. -# -# @example -# echo "$(string::rstrip "Hello World!" "d!")" -# #Output -# Hello Worl -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::rstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1%%$2}" -} - -# @description Make a string lowercase. -# -# @example -# echo "$(string::to_lower "HellO")" -# #Output -# hello -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the lowercased string. -string::to_lower() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1,,}" - else - printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' - fi -} - -# @description Make a string all uppercase. -# -# @example -# echo "$(string::to_upper "HellO")" -# #Output -# HELLO -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the uppercased string. -string::to_upper() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1^^}" - else - printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' - fi -} - -# @description Check whether the search string exists within the input string. -# -# @example -# string::contains "Hello World!" "lo" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_contains hello he - [[ "${1}" == *${2}* ]] -} - -# @description Check whether the input string starts with key string. -# -# @example -# string::starts_with "Hello World!" "He" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::starts_with() { - # Usage: string_starts_with hello he - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - [[ "${1}" == ${2}* ]] -} - -# @description Check whether the input string ends with key string. -# -# @example -# string::ends_with "Hello World!" "d!" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::ends_with() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_ends_wit hello lo - [[ "${1}" == *${2} ]] -} - -# @description Check whether the input string matches the given regex. -# -# @example -# string::regex "HELLO" "^[A-Z]*$" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::regex() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${1} =~ ${2} ]]; then - return 0 - else - return 1 - fi - -} diff --git a/src/configng/functions/bash-utility-master/src/terminal.sh b/src/configng/functions/bash-utility-master/src/terminal.sh deleted file mode 100644 index d73331d7a..000000000 --- a/src/configng/functions/bash-utility-master/src/terminal.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# @file Terminal -# @brief Set of useful terminal functions. - -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -terminal::is_term() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Detect profile rc file for zsh and bash of current script running user. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -# -# @stdout path to the profile file. -terminal::detect_profile() { - declare CURRENT_SHELL="${SHELL##*/}" - case "${CURRENT_SHELL}" in - 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; - 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; - *) if [[ -f "${HOME}/.profile" ]]; then - DETECTED_PROFILE="${HOME}/.profile" - else - printf "No compaitable shell file\n" && exit 1 - fi ;; - esac - printf "%s\n" "${DETECTED_PROFILE}" -} - -# @description Clear the output in terminal on the specified line number. -# This function clears line only on terminal. -# -# @arg $1 Line number to clear. Defaults to 1. (optional) -# -# @exitcode 0 If script is run on terminal. -# -# @stdout clear line ansi code. -terminal::clear_line() { - if terminal::is_term; then - declare line=${1:-1} - printf "\033[%sA\033[2K" "${line}" - fi -} diff --git a/src/configng/functions/bash-utility-master/src/validation.sh b/src/configng/functions/bash-utility-master/src/validation.sh deleted file mode 100644 index 37fde1cbc..000000000 --- a/src/configng/functions/bash-utility-master/src/validation.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env bash - -# @file Validation -# @brief Functions to perform validation on given data. - -# @description Validate whether a given input is a valid email address or not. -# -# @example -# test='test@gmail.com' -# validation::email "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string input email address to validate. -# -# @exitcode 0 If provided input is an email address. -# @exitcode 1 If provided input is not an email address. -# @exitcode 2 Function missing arguments. -validation::email() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare email_re - email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" - [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 -} - -# @description Validate whether a given input is a valid IP V4 address. -# -# @example -# ips=' -# 4.2.2.2 -# a.b.c.d -# 192.168.1.1 -# 0.0.0.0 -# 255.255.255.255 -# 255.255.255.256 -# 192.168.0.1 -# 192.168.0 -# 1234.123.123.123 -# 0.192.168.1 -# ' -# for ip in $ips; do -# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi -# printf "%-20s: %s\n" "$ip" "$stat" -# done -# #Output -# 4.2.2.2 : good -# a.b.c.d : bad -# 192.168.1.1 : good -# 0.0.0.0 : good -# 255.255.255.255 : good -# 255.255.255.256 : bad -# 192.168.0.1 : good -# 192.168.0 : bad -# 1234.123.123.123 : bad -# 0.192.168.1 : good -# -# @arg $1 string input IPv4 address. -# -# @exitcode 0 If provided input is a valid IPv4. -# @exitcode 1 If provided input is not a valid IPv4. -# @exitcode 2 Function missing arguments. -validation::ipv4() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare ip="${1}" - declare IFS=. - # shellcheck disable=SC2206 - declare -a a=($ip) - [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 - # Test values of quads - declare quad - for quad in {0..3}; do - [[ "${a[$quad]}" -gt 255 ]] && return 1 - done - return 0 -} - -# @description Validate whether a given input is a valid IP V6 address. -# -# @example -# ips=' -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 -# fe80::1ff:fe23:4567:890a -# fe80::1ff:fe23:4567:890a%eth2 -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar -# fezy::1ff:fe23:4567:890a -# :: -# 2001:db8:: -# ' -# for ip in $ips; do -# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi -# printf "%-50s= %s\n" "$ip" "$stat" -# done -# #Output -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -# fe80::1ff:fe23:4567:890a = good -# fe80::1ff:fe23:4567:890a%eth2 = good -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -# fezy::1ff:fe23:4567:890a = bad -# :: = good -# 2001:db8:: = good -# -# @arg $1 string input IPv6 address. -# -# @exitcode 0 If provided input is a valid IPv6. -# @exitcode 1 If provided input is not a valid IPv6. -# @exitcode 2 Function missing arguments. -validation::ipv6() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare ip="${1}" - declare 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]))$" - - [[ "${ip}" =~ $re ]] && return 0 || return 1 -} - -# @description Validate if given variable is entirely alphabetic characters. -# -# @example -# test='abcABC' -# validation::alpha "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is only alpha characters. -# @exitcode 1 If input contains any non alpha characters. -# @exitcode 2 Function missing arguments. -validation::alpha() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable contains only alpha-numeric characters. -# -# @example -# test='abc123' -# validation::alpha_num "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is an alpha-numeric. -# @exitcode 1 If input is not an alpha-numeric. -# @exitcode 2 Function missing arguments. -validation::alpha_num() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alnum:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. -# -# @example -# test='abc-ABC_cD' -# validation::alpha_dash "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is valid. -# @exitcode 1 If input the input is not valid. -# @exitcode 2 Function missing arguments. -validation::alpha_dash() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]_-]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. -# -# @arg $1 string Version number to check (eg: 1.0.1) -# $arg $2 string Version number to check (eg: 1.0.1) -# -# @example -# test='abc-ABC_cD' -# validation::version_comparison "12.0.1" "12.0.1" -# echo $? -# #Output -# 0 -# -# @exitcode 0 version number is equal. -# @exitcode 1 $1 version number is greater than $2. -# @exitcode 2 $1 version number is less than $2. -# @exitcode 3 Function is missing required arguments. -# @exitcode 4 Provided input argument is in invalid format. -validation::version_comparison() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 - - declare regex="^[.0-9]*$" - ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 - ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 - - if [[ "$1" == "$2" ]]; then - return 0 - fi - declare IFS=. - declare -a ver1 ver2 - read -r -a ver1 <<<"${1}" - read -r -a ver2 <<<"${2}" - # fill empty fields in ver1 with zeros - for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do - ver1[i]=0 - done - for ((i = 0; i < ${#ver1[@]}; i++)); do - if [[ -z ${ver2[i]} ]]; then - # fill empty fields in ver2 with zeros - ver2[i]=0 - fi - if ((10#${ver1[i]} > 10#${ver2[i]})); then - return 1 - fi - if ((10#${ver1[i]} < 10#${ver2[i]})); then - return 2 - fi - done - return 0 -} diff --git a/src/configng/functions/bash-utility-master/src/variable.sh b/src/configng/functions/bash-utility-master/src/variable.sh deleted file mode 100644 index 67e9fab55..000000000 --- a/src/configng/functions/bash-utility-master/src/variable.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash - -# @file Variable -# @brief Functions for handling variables. - -# @description Check if given variable is array. -# Pass the variable name instead of value of the variable. -# -# @example -# arr=("a" "b" "c") -# variable::is_array "arr" -# #Output -# 0 -# -# @arg $1 string name of the variable to check. -# -# @exitcode 0 If input is array. -# @exitcode 1 If input is not an array. -variable::is_array() { - if [[ -z "${1}" ]]; then - return 1 - else - declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 - fi - return 1 -} - -# @description Check if given variable is a number. -# -# @example -# variable::is_numeric "1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is number. -# @exitcode 1 If input is not a number. -variable::is_numeric() { - declare re='^[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is an integer. -# -# @example -# variable::is_int "+1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is an integer. -# @exitcode 1 If input is not an integer. -variable::is_int() { - declare re='^[+-]?[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a float. -# -# @example -# variable::is_float "+1234.0" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a float. -# @exitcode 1 If input is not a float. -variable::is_float() { - declare re='^[+-]?[0-9]+.?[0-9]*$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a boolean. -# -# @example -# variable::is_bool "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a boolean. -# @exitcode 1 If input is not a boolean. -variable::is_bool() { - [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 -} - -# @description Check if given variable is a true. -# -# @example -# variable::is_true "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is true. -# @exitcode 1 If input is not true. -variable::is_true() { - [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 -} - -# @description Check if given variable is false. -# -# @example -# variable::is_false "false" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is false. -# @exitcode 1 If input is not false. -variable::is_false() { - [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 -} - -# @description Check if given variable is empty or null. -# -# @example -# test='' -# variable::is_empty_or_null $test -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is empty or null. -# @exitcode 1 If input is not empty or null. -variable::is_empty_or_null() { - [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 -} diff --git a/src/configng/functions/cpu.sh b/src/configng/functions/cpu.sh deleted file mode 100644 index 838c6fdbe..000000000 --- a/src/configng/functions/cpu.sh +++ /dev/null @@ -1,199 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. -# - -# @description Return policy as int based on original armbian-config logic. -# -# @example -# cpu::get_policy -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -# -# @stdout Policy as integer. -cpu::get_policy(){ - declare -i policy=0 - [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 - [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 - [[ -d /sys/devices/system/cpu/cpufreq/policy0 && -d /sys/devices/system/cpu/cpufreq/policy2 ]] && policy=2 - printf '%d\n' "$policy" -} - -# @description Return CPU frequencies as string delimited by space. -# -# @example -# cpu::get_freqs 0 -# echo $? -# #Output -# 648000 816000 912000 960000 1008000 1056000 1104000 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout Space delimited string of CPU frequencies. -cpu::get_freqs(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_frequencies" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU minimum frequency as string. -# -# @example -# cpu::get_min_freq 0 -# echo $? -# #Output -# 648000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU minimum frequency as string. -cpu::get_min_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_min_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU maximum frequency as string. -# -# @example -# cpu::get_max_freq 0 -# echo $? -# #Output -# 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU maximum frequency as string. -cpu::get_max_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_max_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governor as string. -# -# @example -# cpu::get_governor 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::get_governor(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_governor" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governors as string delimited by space. -# -# @example -# cpu::get_governors 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::get_governors(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_governors" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Set min, max and CPU governor. -# -# @example -# cpu::set_freq 0 648000 1152000 performance -# echo $? -# #Output -# performance -# -# @arg $1 int Policy. -# @arg $2 int Minimum frequency. -# @arg $3 int Maximum frequency. -# @arg $4 string Governor. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode 3 Invalid minimum frequency. -# @exitcode 4 Invalid maximum frequency. -# @exitcode 5 Minimum frequency must be <= maximum frequency. -# @exitcode 6 Invalid governor. -cpu::set_freq(){ - # Check number of arguments - [[ $# -lt 4 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/etc/default/cpufrequtils" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - declare -i policy=$1 - declare -i min_freq=$2 - declare -i max_freq=$3 - local governor=$4 - # Return frequencies as array - declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) - # Validate minimum frequency - array::contains "$min_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 3 - # Validate maximum frequency - array::contains "$max_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid maximum frequency\n" "${FUNCNAME[0]}" && return 4 - # Validate minimum frequency is <= maximum frequency - [ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 - # Return governors - declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) - # Validate maximum governor - array::contains "$governor" ${governor[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 6 - # Update file - sed -i "s/MIN_SPEED=.*/MIN_SPEED=$min_freq/" "$file" - sed -i "s/MAX_SPEED=.*/MAX_SPEED=$max_freq/" "$file" - sed -i "s/GOVERNOR=.*/GOVERNOR=$governor/" "$file" - # Return value - return 0 -} diff --git a/src/configng/test/cpu_test.sh b/src/configng/test/cpu_test.sh deleted file mode 100644 index f66aecb9d..000000000 --- a/src/configng/test/cpu_test.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related tests. -# - -cur_dir=$(pwd) -# Source bash-utility and cpu functions (this will word under sudo) -source "$cur_dir/../functions/bash-utility-master/src/string.sh" -source "$cur_dir/../functions/bash-utility-master/src/collection.sh" -source "$cur_dir/../functions/bash-utility-master/src/array.sh" -source "$cur_dir/../functions/bash-utility-master/src/check.sh" -source "$cur_dir/../functions/cpu.sh" - -# @description Print value from collection. -# -# @example -# collection::each "print_func" -# #Output -# Value in collection -print_func(){ - printf "%s\n" "$1" - return 0 - } - -# @description Check function exit code and exit script if != 0. -# -# @example -# check_return -# #Output -# Nothing -check_return(){ - if [ "$?" -ne 0 ]; then - exit 1 - fi - } - -# Get policy -declare -i policy=$(cpu::get_policy) -printf 'Policy = %d\n' "$policy" -declare -i min_freq=$(cpu::get_min_freq $policy) -check_return -printf 'Minimum frequency = %d\n' "$min_freq" -declare -i max_freq=$(cpu::get_max_freq $policy) -check_return -printf 'Maximum frequency = %d\n' "$max_freq" -governor=$(cpu::get_governor $policy) -check_return -printf 'Governor = %s\n' "$governor" -# Return frequencies as array -declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) -check_return -printf "\nAll frequencies\n" -# Print all values in collection -printf "%s\n" "${freqs[@]}" | collection::each "print_func" -declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) -check_return -printf "\nAll governors\n" -# Print all values in collection -printf "%s\n" "${governors[@]}" | collection::each "print_func" -# Are we running as sudo? -[[ "$EUID" != 0 ]] && printf "Must call cpu::set_freq as sudo\n" && exit 1 -# Before -printf "\nBefore\n" -cat /etc/default/cpufrequtils -cpu::set_freq $policy "$min_freq" "$max_freq" performance -# After -printf "\nAfter\n" -cat /etc/default/cpufrequtils - From 7d200a267cfa31a10a62a6ad6e12505308714dcf Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 10:50:09 -0700 Subject: [PATCH 18/48] modified: bin/config.sh ajusted code for new names --- bin/config.sh | 7 +- lib/config/extra_drives.sh | 160 ++++++++++++++++++------------------- 2 files changed, 83 insertions(+), 84 deletions(-) diff --git a/bin/config.sh b/bin/config.sh index 6726f29a5..b3eb4fe0c 100644 --- a/bin/config.sh +++ b/bin/config.sh @@ -13,12 +13,11 @@ directory="$(dirname "$(readlink -f "$0")")" filename=$(basename "${BASH_SOURCE[0]}") libpath="$directory/../lib" -#selfpath="$libpath/configng/cpu.sh" if [[ -d "$directory/../lib" ]]; then libpath="$directory"/../lib # installed option todo change when lib location determined -#elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then +#elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/config/" ]]; then # libpath="/usr/lib" else echo "Libraries not found" @@ -29,7 +28,7 @@ fi for file in "$libpath"/bash-utility/*; do source "$file" done -for file in "$libpath"/configng/*; do +for file in "$libpath"/config/*; do source "$file" done @@ -38,7 +37,7 @@ funnamearray=() catagoryarray=() descriptionarray=() -for file in "$libpath"/configng/*.sh; do +for file in "$libpath"/config/*.sh; do mapfile -t temp_functionarray < <(grep -oP '^\w+::\w+' "$file") functionarray+=("${temp_functionarray[@]}") diff --git a/lib/config/extra_drives.sh b/lib/config/extra_drives.sh index 6f5797cd3..771489f86 100644 --- a/lib/config/extra_drives.sh +++ b/lib/config/extra_drives.sh @@ -1,80 +1,80 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Externa Drive related functions. See -# http://linux-mtd.infradead.org/doc/general.html for more info. -# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC - -# @description Set up a simulated MTD spi flash for testing. -# -# @example -# extra_drive::set_spi_vflash -# echo $? -# #Output -# /dev/mtd0 -# /dev/mtd0ro -# /dev/mtdblock0 -# -# @exitcode 0 If successful. -extra_drive::set_spi_vflash(){ - - # Load the nandsim and mtdblock modules to create a virtual MTD device - - sudo modprobe mtdblock - #sudo modprobe nandsim - # Find the newly created MTD device - if [[ ! -e /dev/mtdblock0 ]]; then - sudo modprobe nandsim - irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') - else - echo "$( sudo ls /dev/mtdblock0 )" - fi - - # Create a symlink to the virtual MTD device with the name "spi0.0" - # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name - if [[ ! -e /dev/mtdblock0 ]]; then - ln -s /dev/$virtual_mtd /dev/mtdblock0 - fi - - # Create the mount point if it doesn't exist - mkdir -p /tmp/boot - - # Mount the virtual MTD device to the mount point - mount -t jffs2 /dev/mtdblock0 /tmp/boot - - # write a file to remove - touch /tmp/boot/Mounted_MTD.txt - - echo "$( sudo ls /dev/mtd* )" - -} - - -# @description Remove tsting simulated MTD spi flash. -# -# @example -# extra_drive::rem_spi_vflash -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -extra_drive::rem_spi_vflash(){ - - # Unmount the virtual MTD device from the mount point - umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') - - # Remove the symlink to the virtual MTD device - rm /dev/mtdblock0 - - # Unload the nandsim and mtdblock modules to remove the virtual MTD device - sudo modprobe -r mtdblock - sudo modprobe -r nandsim - - echo "0" -} + +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# Externa Drive related functions. See +# http://linux-mtd.infradead.org/doc/general.html for more info. +# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC + +# @description Set up a simulated MTD spi flash for testing. +# +# @example +# extra_drive::set_spi_vflash +# echo $? +# #Output +# /dev/mtd0 +# /dev/mtd0ro +# /dev/mtdblock0 +# +# @exitcode 0 If successful. +extra_drive::set_spi_vflash(){ + + # Load the nandsim and mtdblock modules to create a virtual MTD device + + sudo modprobe mtdblock + #sudo modprobe nandsim + # Find the newly created MTD device + if [[ ! -e /dev/mtdblock0 ]]; then + sudo modprobe nandsim + irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') + else + echo "$( sudo ls /dev/mtdblock0 )" + fi + + # Create a symlink to the virtual MTD device with the name "spi0.0" + # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name + if [[ ! -e /dev/mtdblock0 ]]; then + ln -s /dev/$virtual_mtd /dev/mtdblock0 + fi + + # Create the mount point if it doesn't exist + mkdir -p /tmp/boot + + # Mount the virtual MTD device to the mount point + mount -t jffs2 /dev/mtdblock0 /tmp/boot + + # write a file to remove + touch /tmp/boot/Mounted_MTD.txt + + echo "$( sudo ls /dev/mtd* )" + +} + + +# @description Remove tsting simulated MTD spi flash. +# +# @example +# extra_drive::rem_spi_vflash +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +extra_drive::rem_spi_vflash(){ + + # Unmount the virtual MTD device from the mount point + umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') + + # Remove the symlink to the virtual MTD device + rm /dev/mtdblock0 + + # Unload the nandsim and mtdblock modules to remove the virtual MTD device + sudo modprobe -r mtdblock + sudo modprobe -r nandsim + + echo "0" +} From 712a725c99d26675620985bc1ab88a465e2b040f Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 11:21:53 -0700 Subject: [PATCH 19/48] modified: lib/config/benchymark.sh --- lib/config/benchymark.sh | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/lib/config/benchymark.sh b/lib/config/benchymark.sh index 90a1ef118..c99351b6a 100644 --- a/lib/config/benchymark.sh +++ b/lib/config/benchymark.sh @@ -23,7 +23,7 @@ # # @stdout tobd. benchymark::see_monitor(){ - [[ $1 == "" ]] && clear && armbianmonitor -M ; + [[ $1 == "" ]] && clear && armbianmonitor -h ; [[ $1 == $1 ]] && armbianmonitor "$1" ; exit 0 } @@ -50,28 +50,12 @@ benchymark::see_monitor(){ # @exitcode 0 If successful. # # @stdout tobd. -benchymark::see_boot_blame(){ - +benchymark::see_boot_times(){ + + [[ $1 == "" ]] && sys_blame=$( systemd-analyze -h ) ; [[ $1 == "blame" ]] && sys_blame=$( systemd-analyze blame ) ; - [[ $1 == "time" || $1 == "" ]] && sys_blame=$( systemd-analyze time ) ; + [[ $1 == "time" ]] && sys_blame=$( systemd-analyze time ) ; [[ $1 == "chain" ]] && sys_blame=$( systemd-analyze critical-chain ) ; printf '%s\n' "${sys_blame[@]}" exit 0 } - - -# @description 7-zip benchmark based on original armbianmonitor logic. -# -# @example -# benchymark::see_7ZipBench -# echo $? -# #Output -# TBD -# -# @exitcode 0 If successful. -# -# @stdout tobd. -benchymark::see_7ZipBench() { - echo -e "Preparing benchmark. Be patient please..." - [[ $1 == "" ]] && armbianmonitor -z ; - } From c98c125d96913ea7085442a3ccdd1c5ac77d3054 Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 12:22:36 -0700 Subject: [PATCH 20/48] deleted: src/ --- bin/configng.sh | 131 - lib/configng/benchymark.sh | 77 - lib/configng/board_led.sh | 63 - lib/configng/cpu.sh | 199 -- lib/configng/vdrive.sh | 80 - src/bash-utility/CODE_OF_CONDUCT.md | 76 - src/bash-utility/CONTRIBUTING.md | 129 - src/bash-utility/LICENSE | 21 - src/bash-utility/README.md | 3026 ----------------- src/bash-utility/bash_utility.sh | 20 - src/bash-utility/bin/bashdoc.awk | 275 -- src/bash-utility/bin/generate_readme.sh | 400 --- src/bash-utility/image/bash-utility.png | Bin 32396 -> 0 bytes src/bash-utility/image/logo.png | Bin 23716 -> 0 bytes src/bash-utility/src/array.sh | 284 -- src/bash-utility/src/check.sh | 34 - src/bash-utility/src/collection.sh | 287 -- src/bash-utility/src/date.sh | 744 ---- src/bash-utility/src/debug.sh | 49 - src/bash-utility/src/file.sh | 222 -- src/bash-utility/src/format.sh | 183 - src/bash-utility/src/interaction.sh | 96 - src/bash-utility/src/json.sh | 25 - src/bash-utility/src/misc.sh | 121 - src/bash-utility/src/os.sh | 135 - src/bash-utility/src/string.sh | 198 -- src/bash-utility/src/terminal.sh | 51 - src/bash-utility/src/validation.sh | 244 -- src/bash-utility/src/variable.sh | 144 - src/configng/README.md | 62 - .../bash-utility-master/CODE_OF_CONDUCT.md | 76 - .../bash-utility-master/CONTRIBUTING.md | 129 - .../functions/bash-utility-master/LICENSE | 21 - .../functions/bash-utility-master/README.md | 3026 ----------------- .../bash-utility-master/bash_utility.sh | 20 - .../bash-utility-master/bin/bashdoc.awk | 275 -- .../bin/generate_readme.sh | 400 --- .../image/bash-utility.png | Bin 32396 -> 0 bytes .../bash-utility-master/image/logo.png | Bin 23716 -> 0 bytes .../bash-utility-master/src/array.sh | 284 -- .../bash-utility-master/src/check.sh | 34 - .../bash-utility-master/src/collection.sh | 287 -- .../functions/bash-utility-master/src/date.sh | 744 ---- .../bash-utility-master/src/debug.sh | 49 - .../functions/bash-utility-master/src/file.sh | 222 -- .../bash-utility-master/src/format.sh | 183 - .../bash-utility-master/src/interaction.sh | 96 - .../functions/bash-utility-master/src/json.sh | 25 - .../functions/bash-utility-master/src/misc.sh | 121 - .../functions/bash-utility-master/src/os.sh | 168 - .../bash-utility-master/src/string.sh | 198 -- .../bash-utility-master/src/terminal.sh | 51 - .../bash-utility-master/src/validation.sh | 244 -- .../bash-utility-master/src/variable.sh | 144 - src/configng/functions/cpu.sh | 199 -- src/configng/test/cpu_test.sh | 75 - 56 files changed, 14447 deletions(-) delete mode 100644 bin/configng.sh delete mode 100644 lib/configng/benchymark.sh delete mode 100644 lib/configng/board_led.sh delete mode 100644 lib/configng/cpu.sh delete mode 100644 lib/configng/vdrive.sh delete mode 100644 src/bash-utility/CODE_OF_CONDUCT.md delete mode 100644 src/bash-utility/CONTRIBUTING.md delete mode 100644 src/bash-utility/LICENSE delete mode 100644 src/bash-utility/README.md delete mode 100644 src/bash-utility/bash_utility.sh delete mode 100644 src/bash-utility/bin/bashdoc.awk delete mode 100644 src/bash-utility/bin/generate_readme.sh delete mode 100644 src/bash-utility/image/bash-utility.png delete mode 100644 src/bash-utility/image/logo.png delete mode 100644 src/bash-utility/src/array.sh delete mode 100644 src/bash-utility/src/check.sh delete mode 100644 src/bash-utility/src/collection.sh delete mode 100644 src/bash-utility/src/date.sh delete mode 100644 src/bash-utility/src/debug.sh delete mode 100644 src/bash-utility/src/file.sh delete mode 100644 src/bash-utility/src/format.sh delete mode 100644 src/bash-utility/src/interaction.sh delete mode 100644 src/bash-utility/src/json.sh delete mode 100644 src/bash-utility/src/misc.sh delete mode 100644 src/bash-utility/src/os.sh delete mode 100644 src/bash-utility/src/string.sh delete mode 100644 src/bash-utility/src/terminal.sh delete mode 100644 src/bash-utility/src/validation.sh delete mode 100644 src/bash-utility/src/variable.sh delete mode 100644 src/configng/README.md delete mode 100644 src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md delete mode 100644 src/configng/functions/bash-utility-master/CONTRIBUTING.md delete mode 100644 src/configng/functions/bash-utility-master/LICENSE delete mode 100644 src/configng/functions/bash-utility-master/README.md delete mode 100644 src/configng/functions/bash-utility-master/bash_utility.sh delete mode 100644 src/configng/functions/bash-utility-master/bin/bashdoc.awk delete mode 100644 src/configng/functions/bash-utility-master/bin/generate_readme.sh delete mode 100644 src/configng/functions/bash-utility-master/image/bash-utility.png delete mode 100644 src/configng/functions/bash-utility-master/image/logo.png delete mode 100644 src/configng/functions/bash-utility-master/src/array.sh delete mode 100644 src/configng/functions/bash-utility-master/src/check.sh delete mode 100644 src/configng/functions/bash-utility-master/src/collection.sh delete mode 100644 src/configng/functions/bash-utility-master/src/date.sh delete mode 100644 src/configng/functions/bash-utility-master/src/debug.sh delete mode 100644 src/configng/functions/bash-utility-master/src/file.sh delete mode 100644 src/configng/functions/bash-utility-master/src/format.sh delete mode 100644 src/configng/functions/bash-utility-master/src/interaction.sh delete mode 100644 src/configng/functions/bash-utility-master/src/json.sh delete mode 100644 src/configng/functions/bash-utility-master/src/misc.sh delete mode 100644 src/configng/functions/bash-utility-master/src/os.sh delete mode 100644 src/configng/functions/bash-utility-master/src/string.sh delete mode 100644 src/configng/functions/bash-utility-master/src/terminal.sh delete mode 100644 src/configng/functions/bash-utility-master/src/validation.sh delete mode 100644 src/configng/functions/bash-utility-master/src/variable.sh delete mode 100644 src/configng/functions/cpu.sh delete mode 100644 src/configng/test/cpu_test.sh diff --git a/bin/configng.sh b/bin/configng.sh deleted file mode 100644 index 6726f29a5..000000000 --- a/bin/configng.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# - -directory="$(dirname "$(readlink -f "$0")")" -filename=$(basename "${BASH_SOURCE[0]}") - -libpath="$directory/../lib" -#selfpath="$libpath/configng/cpu.sh" - -if [[ -d "$directory/../lib" ]]; then - libpath="$directory"/../lib -# installed option todo change when lib location determined -#elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/configng/" ]]; then -# libpath="/usr/lib" -else - echo "Libraries not found" - exit 0 -fi - -# Source the files relative to the script location -for file in "$libpath"/bash-utility/*; do - source "$file" -done -for file in "$libpath"/configng/*; do - source "$file" -done - -functionarray=() -funnamearray=() -catagoryarray=() -descriptionarray=() - -for file in "$libpath"/configng/*.sh; do - mapfile -t temp_functionarray < <(grep -oP '^\w+::\w+' "$file") - functionarray+=("${temp_functionarray[@]}") - - mapfile -t temp_funnamearray < <(grep -oP '^\w+::\w+' "$file" | sed 's/.*:://') - funnamearray+=("${temp_funnamearray[@]}") - - mapfile -t temp_catagoryarray < <(grep -oP '^\w+::\w+' "$file" | sed 's/::.*//') - catagoryarray+=("${temp_catagoryarray[@]}") - - mapfile -t temp_descriptionarray < <(grep -oP '^# @description.*' "$file" | sed 's/^# @description //') - descriptionarray+=("${temp_descriptionarray[@]}") -done - - see_help_dev(){ - # Extract unique prefixes - declare -A prefixes - for i in "${!functionarray[@]}"; do - prefix="${functionarray[i]%%::*}" - prefixes["$prefix"]=1 - done - - # Construct usage line - usage="" - for prefix in "${!prefixes[@]}"; do - usage+="[ $prefix::options ]" - done - - -# echo "$usage" - echo "Usage: ${filename%.*} [ -h | foo ]" - echo "" - echo -e "Options:" - echo -e " -h) Print this help." - echo -e "" - echo -e " foo) Usage: ${filename%.*} foo $usage:: " - echo "" - - # Group options by prefix - declare -A groups - for i in "${!functionarray[@]}"; do - prefix="${functionarray[i]%%::*}" - suffix="${functionarray[i]#*::}" - groups["$prefix"]+=$'\t\t'"$suffix\t${descriptionarray[i]}"$'\n' - done - - # Print grouped options - for group in "${!groups[@]}"; do - echo -e " $group::options" - echo -e "${groups[$group]}" - done -} - - -# TEST 7 -# check for -h -dev @ $1 -# if -dev check @ $1 remove and shift $1 to check for x -check_opts() { - if [ "$1" == "foo" ]; then - shift # Shifts the arguments to the left, excluding the first argument ("-dev") - function_name="$1" # Assigns the next argument as the function name - shift # Shifts the arguments again to exclude the function name - - found=false - - for ((i=0; i<${#functionarray[@]}; i++)); do - if [ "$function_name" == "${functionarray[i]}" ]; then - found=true - ${functionarray[i]} "$@" - break - fi - done - - if [ "$found" == false ]; then - echo "Invalid function name" - see_help_dev - exit 1 - - fi - - elif [[ "$1" == "foo" && "$2" == "cpu::set_freq" ]]; then - # Disabled till understood. - echo "cpu::set_freq policy min_freq max_freq performance" - echo "Disabled durring current testing" - - else - see_help_dev - fi -} - -check_opts "$@" diff --git a/lib/configng/benchymark.sh b/lib/configng/benchymark.sh deleted file mode 100644 index 90a1ef118..000000000 --- a/lib/configng/benchymark.sh +++ /dev/null @@ -1,77 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Benchmark related functions. See -# https://systemd.io/ for more info. -# https://www.7-zip.org/ - -# @description system boot-up performance statistics. -# -# @example -# benchymark::see_systemd $1 (blame time chain) -# #Output -# time (quick check of boot time) -# balme (List modual load times) -# chain () -# -# @exitcode 0 If successful. -# -# @stdout tobd. -benchymark::see_monitor(){ - [[ $1 == "" ]] && clear && armbianmonitor -M ; - [[ $1 == $1 ]] && armbianmonitor "$1" ; - exit 0 - } -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Benchmark related functions. See -# https://systemd.io/ for more info. -# https://www.7-zip.org/ - -# @description system boot-up performance statistics. -# -# @example -# benchymark::see_systemd $1 (blame time chain) -# #Output -# time (quick check of boot time) -# balme (List modual load times) -# chain () -# -# @exitcode 0 If successful. -# -# @stdout tobd. -benchymark::see_boot_blame(){ - - [[ $1 == "blame" ]] && sys_blame=$( systemd-analyze blame ) ; - [[ $1 == "time" || $1 == "" ]] && sys_blame=$( systemd-analyze time ) ; - [[ $1 == "chain" ]] && sys_blame=$( systemd-analyze critical-chain ) ; - printf '%s\n' "${sys_blame[@]}" - exit 0 - } - - -# @description 7-zip benchmark based on original armbianmonitor logic. -# -# @example -# benchymark::see_7ZipBench -# echo $? -# #Output -# TBD -# -# @exitcode 0 If successful. -# -# @stdout tobd. -benchymark::see_7ZipBench() { - echo -e "Preparing benchmark. Be patient please..." - [[ $1 == "" ]] && armbianmonitor -z ; - } diff --git a/lib/configng/board_led.sh b/lib/configng/board_led.sh deleted file mode 100644 index 9eea83fa0..000000000 --- a/lib/configng/board_led.sh +++ /dev/null @@ -1,63 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# System boards led monitoring. See -# TBD - -# @description set the Sys board led to montor cpu activity. -# -# @example -# boardLED::set_sysled_cpu -# #Output -# Led blinks to set cpu -# -# @exitcode 0 If successful. -# -# @stdout cpu. -boardled::set_sysled_cpu(){ - - echo cpu | sudo tee /sys/class/leds/*/trigger - - } - -# @description set the Sys board led to montor none. -# -# @example -# boardLED::set_sysled_none -# #Output -# Led blinks to set no -# -# @exitcode 0 If successful. -# -# @stdout none. -boardled::set_sysled_none(){ - - echo none | sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger - } - -# @description See a list of board led options. -# -# @example -# boardLED::set_sysled_none -# #Output -# Led blinks to set no -# -# @exitcode 0 If successful. -# -# @stdout tbd. -boardled::see_sysled(){ - - # the avalible options - readarray triggers_led < <( cat /sys/class/leds/*/trigger ) - # see pass not argument the avalible options - [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; - # Set the systme Led blink to $1 valus - [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; - -} - diff --git a/lib/configng/cpu.sh b/lib/configng/cpu.sh deleted file mode 100644 index 6847be043..000000000 --- a/lib/configng/cpu.sh +++ /dev/null @@ -1,199 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. - -# @description Return policy as int based on original armbian-config logic. -# -# @example -# cpu::get_policy -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -# -# @stdout Policy as integer. -cpu::see_policy(){ - declare -i policy=0 - [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 - [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 - [[ -d /sys/devices/system/cpu/cpufreq/policy0 && -d /sys/devices/system/cpu/cpufreq/policy2 ]] && policy=2 - printf '%d\n' "$policy" -} - -# @description Return CPU frequencies as string delimited by space. -# -# @example -# cpu::get_freqs 0 -# echo $? -# #Output -# 648000 816000 912000 960000 1008000 1056000 1104000 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout Space delimited string of CPU frequencies. -cpu::see_freqs(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_frequencies" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU minimum frequency as string. -# -# @example -# cpu::get_min_freq 0 -# echo $? -# #Output -# 648000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU minimum frequency as string. -cpu::see_min_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_min_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU maximum frequency as string. -# -# @example -# cpu::get_max_freq 0 -# echo $? -# #Output -# 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU maximum frequency as string. -cpu::see_max_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_max_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governor as string. -# -# @example -# cpu::get_governor 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::see_governor(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_governor" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governors as string delimited by space. -# -# @example -# cpu::get_governors 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::see_governors(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_governors" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Set min, max and CPU governor. -# -# @example -# cpu::set_freq 0 648000 1152000 performance -# echo $? -# #Output -# performance -# -# @arg $1 int Policy. -# @arg $2 int Minimum frequency. -# @arg $3 int Maximum frequency. -# @arg $4 string Governor. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode 3 Invalid minimum frequency. -# @exitcode 4 Invalid maximum frequency. -# @exitcode 5 Minimum frequency must be <= maximum frequency. -# @exitcode 6 Invalid governor. -cpu::set_freq(){ - # Check number of arguments - [[ $# -lt 4 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/etc/default/cpufrequtils" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - declare -i policy=$1 - declare -i min_freq=$2 - declare -i max_freq=$3 - local governor=$4 - # Return frequencies as array - declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) - # Validate minimum frequency - array::contains "$min_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 3 - # Validate maximum frequency - array::contains "$max_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid maximum frequency\n" "${FUNCNAME[0]}" && return 4 - # Validate minimum frequency is <= maximum frequency - [ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 - # Return governors - declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) - # Validate maximum governor - array::contains "$governor" ${governor[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 6 - # Update file - sed -i "s/MIN_SPEED=.*/MIN_SPEED=$min_freq/" "$file" - sed -i "s/MAX_SPEED=.*/MAX_SPEED=$max_freq/" "$file" - sed -i "s/GOVERNOR=.*/GOVERNOR=$governor/" "$file" - # Return value - return 0 -} - diff --git a/lib/configng/vdrive.sh b/lib/configng/vdrive.sh deleted file mode 100644 index 771489f86..000000000 --- a/lib/configng/vdrive.sh +++ /dev/null @@ -1,80 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Externa Drive related functions. See -# http://linux-mtd.infradead.org/doc/general.html for more info. -# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC - -# @description Set up a simulated MTD spi flash for testing. -# -# @example -# extra_drive::set_spi_vflash -# echo $? -# #Output -# /dev/mtd0 -# /dev/mtd0ro -# /dev/mtdblock0 -# -# @exitcode 0 If successful. -extra_drive::set_spi_vflash(){ - - # Load the nandsim and mtdblock modules to create a virtual MTD device - - sudo modprobe mtdblock - #sudo modprobe nandsim - # Find the newly created MTD device - if [[ ! -e /dev/mtdblock0 ]]; then - sudo modprobe nandsim - irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') - else - echo "$( sudo ls /dev/mtdblock0 )" - fi - - # Create a symlink to the virtual MTD device with the name "spi0.0" - # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name - if [[ ! -e /dev/mtdblock0 ]]; then - ln -s /dev/$virtual_mtd /dev/mtdblock0 - fi - - # Create the mount point if it doesn't exist - mkdir -p /tmp/boot - - # Mount the virtual MTD device to the mount point - mount -t jffs2 /dev/mtdblock0 /tmp/boot - - # write a file to remove - touch /tmp/boot/Mounted_MTD.txt - - echo "$( sudo ls /dev/mtd* )" - -} - - -# @description Remove tsting simulated MTD spi flash. -# -# @example -# extra_drive::rem_spi_vflash -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -extra_drive::rem_spi_vflash(){ - - # Unmount the virtual MTD device from the mount point - umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') - - # Remove the symlink to the virtual MTD device - rm /dev/mtdblock0 - - # Unload the nandsim and mtdblock modules to remove the virtual MTD device - sudo modprobe -r mtdblock - sudo modprobe -r nandsim - - echo "0" -} diff --git a/src/bash-utility/CODE_OF_CONDUCT.md b/src/bash-utility/CODE_OF_CONDUCT.md deleted file mode 100644 index 461c926b9..000000000 --- a/src/bash-utility/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at - -For answers to common questions about this code of conduct, see - - -[homepage]: https://www.contributor-covenant.org diff --git a/src/bash-utility/CONTRIBUTING.md b/src/bash-utility/CONTRIBUTING.md deleted file mode 100644 index aa401df88..000000000 --- a/src/bash-utility/CONTRIBUTING.md +++ /dev/null @@ -1,129 +0,0 @@ -# Contributing to Bash-Utility - -:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: -The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. - -## Table of Contents -- [Code Contributions](#code-contributions) -- [Code Guidelines](#code-guidelines) - - [Styleguide](#styleguide) - - [Bashdoc guideline](#bashdoc-guideline) -- [Documentation](#documentation) -- [Commit Guidelines](#commit-guidelines) -- [Pull Request Guidelines](#pull-request-guidelines) -- [Contact](#contact) - -## Code Contributions - -Great, the more, the merrier. - -Sane code contributions are always welcome, whether to the code or documentation. - -Before making a pull request, make sure to follow below guidelines: - -### Code Guidelines - -#### Styleguide - -- Variable names must be meaningful and self-documenting. -- Long variable names must be structured by underscores to improve legibility. -- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE) -- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data) -- Variable names can be alphanumeric with underscores. No special characters in variable names. -- Variables name must not start with number. -- Variables within function must be declared. So the scope of variable is restricted to the function. -- Avoid accessing global variables within functions. -- Function names must be all lower case with underscores to seperate words (snake_case). -- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`) -- Try using bash builtins and string substitution as much as possible. -- Use printf everywhere instead of echo. -- Before adding a new logic, be sure to check the existing code. -- Make sure to add the function in appropriate section based on its operation. -- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command: - - ```shell - shfmt upload.sh - ``` - - The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags. - You can also install shfmt for various editors, refer their repo for information. - Note: This is strictly necessary to maintain consistency, do not skip. - -- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason. - -#### Bashdoc guideline - -The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is -properly generated by the generator. - -Follow the below bashdoc template to add function introductory comment. - -```bash -# @description Multiline description goes here and -# there -# -# @example -# sample::function a b c -# echo 123 -# -# @arg $1 string Some arg. -# @arg $2 any Rest of arguments. -# -# @noargs -# -# @exitcode 0 If successfull. -# @exitcode >0 On failure -# @exitcode 5 On some error. -# -# @stdout Path to something. -# -# @see sample::other_function(() -sample::function() { -} -``` - -- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used. -- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment. -- Make sure to document the exitcode emitted by the function. -- If the function is similar to other function add a reference to function using @see tag. - -### Documentation - -- Refrain from making unnecessary newlines or whitespace. -- Use pure markdown as much as possible, html is accepted but shouldn't be a priority. -- The markdown must pass RemarkLint checks. -- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc. - - ```bash - ./bin/generate_readme.sh -f README.md -s src/ - ``` - -### Commit Guidelines - -It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted. - -It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message. - -The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation. - -Before committing check for unnecessary whitespace with `git diff --check`. - -### Pull Request Guidelines - -The following guidelines will increase the likelihood that your pull request will get accepted: - -- Follow the commit and code guidelines. -- Keep the patches on topic and focused. -- Try to avoid unnecessary formatting and clean-up where reasonable. - -A pull request should contain the following: - -- At least one commit (all of which should follow the Commit Guidelines). -- Title that summarises the issue/feature. -- Description that briefly summarises the changes. - -After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread. - -## Contact - -For further inquiries, you can contact the developer by opening an issue on the repository. diff --git a/src/bash-utility/LICENSE b/src/bash-utility/LICENSE deleted file mode 100644 index 99dd0836b..000000000 --- a/src/bash-utility/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 labbots - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/bash-utility/README.md b/src/bash-utility/README.md deleted file mode 100644 index 9bc1f9484..000000000 --- a/src/bash-utility/README.md +++ /dev/null @@ -1,3026 +0,0 @@ -

Bash Utility

- -

-Stars -License - -

-

-Gh-pages Status -Website -

-

-Total number of Library functions -

-

- -

-Bash library which provides utility functions and helpers for functional programming in Bash. - -Detailed documentation is available at - - - -## Table of Contents - -- [Installation](#installation) - - [Method 1 - Git Submodules](#method-1---git-submodules) - - [Method 2 - Git Clone](#method-2---git-clone) - - [Method 3 - Direct Download](#method-3---direct-download) -- [Usage](#usage) -- [Array](#array) - - [array::contains()](#arraycontains) - - [array::dedupe()](#arraydedupe) - - [array::is_empty()](#arrayis_empty) - - [array::join()](#arrayjoin) - - [array::reverse()](#arrayreverse) - - [array::random_element()](#arrayrandom_element) - - [array::sort()](#arraysort) - - [array::rsort()](#arrayrsort) - - [array::bsort()](#arraybsort) - - [array::merge()](#arraymerge) -- [Check](#check) - - [check::command_exists()](#checkcommand_exists) - - [check::is_sudo()](#checkis_sudo) -- [Collection](#collection) - - [collection::each()](#collectioneach) - - [collection::every()](#collectionevery) - - [collection::filter()](#collectionfilter) - - [collection::find()](#collectionfind) - - [collection::invoke()](#collectioninvoke) - - [collection::map()](#collectionmap) - - [collection::reject()](#collectionreject) - - [collection::some()](#collectionsome) -- [Date](#date) - - [date::now()](#datenow) - - [date::epoc()](#dateepoc) - - [date::add_days_from()](#dateadd_days_from) - - [date::add_months_from()](#dateadd_months_from) - - [date::add_years_from()](#dateadd_years_from) - - [date::add_weeks_from()](#dateadd_weeks_from) - - [date::add_hours_from()](#dateadd_hours_from) - - [date::add_minutes_from()](#dateadd_minutes_from) - - [date::add_seconds_from()](#dateadd_seconds_from) - - [date::add_days()](#dateadd_days) - - [date::add_months()](#dateadd_months) - - [date::add_years()](#dateadd_years) - - [date::add_weeks()](#dateadd_weeks) - - [date::add_hours()](#dateadd_hours) - - [date::add_minutes()](#dateadd_minutes) - - [date::add_seconds()](#dateadd_seconds) - - [date::sub_days_from()](#datesub_days_from) - - [date::sub_months_from()](#datesub_months_from) - - [date::sub_years_from()](#datesub_years_from) - - [date::sub_weeks_from()](#datesub_weeks_from) - - [date::sub_hours_from()](#datesub_hours_from) - - [date::sub_minutes_from()](#datesub_minutes_from) - - [date::sub_seconds_from()](#datesub_seconds_from) - - [date::sub_days()](#datesub_days) - - [date::sub_months()](#datesub_months) - - [date::sub_years()](#datesub_years) - - [date::sub_weeks()](#datesub_weeks) - - [date::sub_hours()](#datesub_hours) - - [date::sub_minutes()](#datesub_minutes) - - [date::sub_seconds()](#datesub_seconds) - - [date::format()](#dateformat) -- [Debug](#debug) - - [debug::print_array()](#debugprint_array) - - [debug::print_ansi()](#debugprint_ansi) -- [File](#file) - - [file::make_temp_file()](#filemake_temp_file) - - [file::make_temp_dir()](#filemake_temp_dir) - - [file::name()](#filename) - - [file::basename()](#filebasename) - - [file::extension()](#fileextension) - - [file::dirname()](#filedirname) - - [file::full_path()](#filefull_path) - - [file::mime_type()](#filemime_type) - - [file::contains_text()](#filecontains_text) -- [Format](#format) - - [format::human_readable_seconds()](#formathuman_readable_seconds) - - [format::bytes_to_human()](#formatbytes_to_human) - - [format::strip_ansi()](#formatstrip_ansi) - - [format::text_center()](#formattext_center) - - [format::report()](#formatreport) - - [format::trim_text_to_term()](#formattrim_text_to_term) -- [Interaction](#interaction) - - [interaction::prompt_yes_no()](#interactionprompt_yes_no) - - [interaction::prompt_response()](#interactionprompt_response) -- [Json](#json) - - [json::get_value()](#jsonget_value) -- [Miscellaneous](#miscellaneous) - - [misc::check_internet_connection()](#misccheck_internet_connection) - - [misc::get_pid()](#miscget_pid) - - [misc::get_uid()](#miscget_uid) - - [misc::generate_uuid()](#miscgenerate_uuid) -- [Operating System](#operating-system) - - [os::detect_os()](#osdetect_os) - - [os::detect_linux_distro()](#osdetect_linux_distro) - - [os::detect_linux_version()](#osdetect_linux_version) - - [os::detect_mac_version()](#osdetect_mac_version) -- [String](#string) - - [string::trim()](#stringtrim) - - [string::split()](#stringsplit) - - [string::lstrip()](#stringlstrip) - - [string::rstrip()](#stringrstrip) - - [string::to_lower()](#stringto_lower) - - [string::to_upper()](#stringto_upper) - - [string::contains()](#stringcontains) - - [string::starts_with()](#stringstarts_with) - - [string::ends_with()](#stringends_with) - - [string::regex()](#stringregex) -- [Terminal](#terminal) - - [terminal::is_term()](#terminalis_term) - - [terminal::detect_profile()](#terminaldetect_profile) - - [terminal::clear_line()](#terminalclear_line) -- [Validation](#validation) - - [validation::email()](#validationemail) - - [validation::ipv4()](#validationipv4) - - [validation::ipv6()](#validationipv6) - - [validation::alpha()](#validationalpha) - - [validation::alpha_num()](#validationalpha_num) - - [validation::alpha_dash()](#validationalpha_dash) - - [validation::version_comparison()](#validationversion_comparison) -- [Variable](#variable) - - [variable::is_array()](#variableis_array) - - [variable::is_numeric()](#variableis_numeric) - - [variable::is_int()](#variableis_int) - - [variable::is_float()](#variableis_float) - - [variable::is_bool()](#variableis_bool) - - [variable::is_true()](#variableis_true) - - [variable::is_false()](#variableis_false) - - [variable::is_empty_or_null()](#variableis_empty_or_null) -- [Inspired By](#inspired-by) -- [License](#license) - - -## Installation -The script can be installed and sourced using following methods. - -### Method 1 - Git Submodules -If the library is used inside a git project then git submodules can be used to install the library to the project. -Following command will initialize git submodule and download the library to `./vendor/bash-utility` folder. - -```shell -git submodule init -git submodule add -b master https://github.com/labbots/bash-utility vendor/bash-utility -``` - -To Update submodules to latest code execute the following command. - -```shell -git submodule update --rebase --remote -``` -Once the submodule is added or updated, make sure to commit changes to your repository. - -```shell -git add . -git commit -m 'Added/updated bash-utility library.' -``` -**Note:** When cloning your repository, use `--recurse-submodules` flag to `git clone` command to install the git sub modules. - -### Method 2 - Git Clone -If you don't want to use git submodules, you can use `git clone` to download library and then move the files to desired location manually. - -The below command will clone the repository to `vendor/bash-utility` folder in current working directory. - -```shell -git clone https://github.com/labbots/bash-utility.git ./vendor/bash-utility -``` -### Method 3 - Direct Download -If you do not have git installed, you can download the archive of the latest version of the library. Extract the zip file to appropriate folder by following the below command. - -```shell -wget https://github.com/labbots/bash-utility/archive/master.zip -unzip -q master.zip -d tmp -mkdir -p vendor/bash-utility -mv tmp/bash-utility-master vendor/bash-utility -rm tmp -``` - -## Usage -Bash utility functions can be used by simply sourcing the library script file to your own script. -To access all the functions within the bash-utility library, you could import the main bash file as follows. - -```shell -source "vendor/bash-utility/bash-utility.sh" -``` - -You can also only use the necessary library functions by only importing the required function files. - -```shell -source "vendor/bash-utility/src/array.sh" -``` - - - -## Array - -Functions for array operations and manipulations. - -### array::contains() - -Check if item exists in the given array. - -#### Arguments - -- **$1** (mixed): Item to search (needle). -- **$2** (array): array to be searched (haystack). - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found in the array. -- **2**: Function missing arguments. - -#### Example - -```bash -array=("a" "b" "c") -array::contains "c" ${array[@]} -#Output -0 -``` - -### array::dedupe() - -Remove duplicate items from the array. - -#### Arguments - -- **$1** (array): Array to be deduped. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Deduplicated array. - -#### Example - -```bash -array=("a" "b" "a" "c") -printf "%s" "$(array::dedupe ${array[@]})" -#Output -a -b -c -``` - -### array::is_empty() - -Check if a given array is empty. - -#### Arguments - -- **$1** (array): Array to be checked. - -#### Exit codes - -- **0**: If the given array is empty. -- **2**: If the given array is not empty. - -#### Example - -```bash -array=("a" "b" "c" "d") -array::is_empty "${array[@]}" -``` - -### array::join() - -Join array elements with a string. - -#### Arguments - -- **$1** (string): String to join the array elements (glue). -- **$2** (array): array to be joined with glue string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- String containing a string representation of all the array elements in the same order,with the glue string between each element. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s" "$(array::join "," "${array[@]}")" -#Output -a,b,c,d -printf "%s" "$(array::join "" "${array[@]}")" -#Output -abcd -``` - -### array::reverse() - -Return an array with elements in reverse order. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The reversed array. - -#### Example - -```bash -array=(1 2 3 4 5) -printf "%s" "$(array::reverse "${array[@]}")" -#Output -5 4 3 2 1 -``` - -### array::random_element() - -Returns a random item from the array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Random item out of the array. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s\n" "$(array::random_element "${array[@]}")" -#Output -c -``` - -### array::sort() - -Sort an array from lowest to highest. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -1 -2 -4 5 -a -a c -d -``` - -### array::rsort() - -Sort an array in reverse order (highest to lowest). - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- reverse sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -d -a c -a -4 5 -2 -1 -``` - -### array::bsort() - -Bubble sort an integer array from lowest to highest. -This sort does not work on string array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- bubble sorted array. - -#### Example - -```bash -iarr=(4 5 1 3) -array::bsort "${iarr[@]}" -#Output -1 -3 -4 -5 -``` - -### array::merge() - -Merge two arrays. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of first array. -- **$2** (string): variable name of second array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Merged array. - -#### Example - -```bash -a=("a" "c") -b=("d" "c") -array::merge "a[@]" "b[@]" -#Output -a -c -d -c -``` - -## Check - -Helper functions. - -### check::command_exists() - -Check if the command exists in the system. - -#### Arguments - -- **$1** (string): Command name to be searched. - -#### Exit codes - -- **0**: If the command exists. -- **1**: If the command does not exist. -- **2**: Function missing arguments. - -#### Example - -```bash -check::command_exists "tput" -``` - -### check::is_sudo() - -Check if the script is executed with sudo privilege. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If the script is executed with root privilege. -- **1**: If the script is not executed with root privilege - -#### Example - -```bash -check::is_sudo -``` - -## Collection - -(Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -### collection::each() - -Iterates over elements of collection and invokes iteratee for each element. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output of iteratee function. - -#### Example - -```bash -test_func(){ - printf "print value: %s\n" "$1" - return 0 - } -arr1=("a b" "c d" "a" "d") -printf "%s\n" "${arr1[@]}" | collection::each "test_func" -collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -#output - print value: a b - print value: c d - print value: a - print value: d -``` - -#### Example - -```bash -# If other function from this library is already used to process the array. -# Then following method could be used to pass the array to the function. -out=("$(array::dedupe "${arr1[@]}")") -collection::each "test_func" <<< "${out[@]}" -``` - -### collection::every() - -Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If iteratee function fails. -- **2**: Function missing arguments. - -#### Example - -```bash -arri=("1" "2" "3" "4") -printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -``` - -### collection::filter() - -Iterates over elements of array, returning all elements where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -#output -1 -2 -3 -``` - -### collection::find() - -Iterates over elements of collection, returning the first element where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Output on stdout - -- first array value matching the iteratee function. - -#### Example - -```bash -arr=("1" "2" "3" "a") -check_a(){ - [[ "$1" = "a" ]] -} -printf "%s\n" "${arr[@]}" | collection::find "check_a" -#Output -a -``` - -### collection::invoke() - -Invokes the iteratee with each element passed as argument to the iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output from the iteratee function. - -#### Example - -```bash -opt=("-a" "-l") -printf "%s\n" "${opt[@]}" | collection::invoke "ls" -``` - -### collection::map() - -Creates an array of values by running each element in array through iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output result of iteratee on value. - -#### Example - -```bash -arri=("1" "2" "3") -add_one(){ - i=${1} - i=$(( i + 1 )) - printf "%s\n" "$i" -} -printf "%s\n" "${arri[@]}" | collection::map "add_one" -``` - -### collection::reject() - -The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values not matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -#Ouput -a -``` - -#### See also - -- [collection::filter](#collectionfilter) - -### collection::some() - -Checks if iteratee returns true for any element of the array. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If match successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -arr=("a" "b" "3" "a") -printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -``` - -## Date - -Functions for manipulating dates. - -### date::now() - -Get current time in unix timestamp. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- current timestamp. - -#### Example - -```bash -echo "$(date::now)" -#Output -1591554426 -``` - -### date::epoc() - -convert datetime string to unix timestamp. - -#### Arguments - -- **$1** (string): date time in any format. - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp for specified datetime. - -#### Example - -```bash -echo "$(date::epoc "2020-07-07 18:38")" -#Output -1594143480 -``` - -### date::add_days_from() - -Add number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days_from "1594143480")" -#Output -1594229880 -``` - -### date::add_months_from() - -Add number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months_from "1594143480")" -#Output -1596821880 -``` - -### date::add_years_from() - -Add number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years_from "1594143480")" -#Output -1625679480 -``` - -### date::add_weeks_from() - -Add number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks_from "1594143480")" -#Output -1594748280 -``` - -### date::add_hours_from() - -Add number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours_from "1594143480")" -#Output -1594147080 -``` - -### date::add_minutes_from() - -Add number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes_from "1594143480")" -#Output -1594143540 -``` - -### date::add_seconds_from() - -Add number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds_from "1594143480")" -#Output -1594143481 -``` - -### date::add_days() - -Add number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days "1")" -#Output -1591640826 -``` - -### date::add_months() - -Add number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months "1")" -#Output -1594146426 -``` - -### date::add_years() - -Add number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years "1")" -#Output -1623090426 -``` - -### date::add_weeks() - -Add number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks "1")" -#Output -1592159226 -``` - -### date::add_hours() - -Add number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours "1")" -#Output -1591558026 -``` - -### date::add_minutes() - -Add number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes "1")" -#Output -1591554486 -``` - -### date::add_seconds() - -Add number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds "1")" -#Output -1591554427 -``` - -### date::sub_days_from() - -Subtract number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days_from "1594143480")" -#Output -1594057080 -``` - -### date::sub_months_from() - -Subtract number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months_from "1594143480")" -#Output -1591551480 -``` - -### date::sub_years_from() - -Subtract number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years_from "1594143480")" -#Output -1562521080 -``` - -### date::sub_weeks_from() - -Subtract number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks_from "1594143480")" -#Output -1593538680 -``` - -### date::sub_hours_from() - -Subtract number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours_from "1594143480")" -#Output -1594139880 -``` - -### date::sub_minutes_from() - -Subtract number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes_from "1594143480")" -#Output -1594143420 -``` - -### date::sub_seconds_from() - -Subtract number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds_from "1594143480")" -#Output -1594143479 -``` - -### date::sub_days() - -Subtract number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days "1")" -#Output -1588876026 -``` - -### date::sub_months() - -Subtract number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months "1")" -#Output -1559932026 -``` - -### date::sub_years() - -Subtract number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years "1")" -#Output -1591468026 -``` - -### date::sub_weeks() - -Subtract number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks "1")" -#Output -1590949626 -``` - -### date::sub_hours() - -Subtract number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours "1")" -#Output -1591550826 -``` - -### date::sub_minutes() - -Subtract number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes "1")" -#Output -1591554366 -``` - -### date::sub_seconds() - -Subtract number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds "1")" -#Output -1591554425 -``` - -### date::format() - -Format unix timestamp to human readable format. -If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (string): format control characters based on `date` command (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate time string. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo echo "$(date::format "1594143480")" -#Output -2020-07-07 18:38:00 -``` - -## Debug - -Functions to facilitate debugging scripts. - -### debug::print_array() - -Prints the content of array as key value pair for easier debugging. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of the array. - -#### Output on stdout - -- Formatted key value of array. - -#### Example - -```bash -array=(foo bar baz) -printf "Array\n" -printarr "array" -declare -A assoc_array -assoc_array=([foo]=bar [baz]=foobar) -printf "Assoc Array\n" -printarr "assoc_array" -#Output -Array -0 = foo -1 = bar -2 = baz -Assoc Array -baz = foobar -foo = bar -``` - -### debug::print_ansi() - -Function to print ansi escape sequence as is. -This function helps debug ansi escape sequence in text by displaying the escape codes. - -#### Arguments - -- **$1** (string): input with ansi escape sequence. - -#### Output on stdout - -- Ansi escape sequence printed in output as is. - -#### Example - -```bash -txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -debug::print_ansi "$txt" -#Output -\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -``` - -## File - -Functions for handling files. - -### file::make_temp_file() - -Create temporary file. -Function creates temporary file with random name. The temporary file will be deleted when script finishes. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp file. - -#### Output on stdout - -- file name of temporary file created. - -#### Example - -```bash -echo "$(file::make_temp_file)" -#Output -tmp.vgftzy -``` - -### file::make_temp_dir() - -Create temporary directory. -Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. - -#### Arguments - -- **$1** (string): Temporary directory prefix -- $2 string Flag to auto remove directory on exit trap (true) - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp directory. -- **2**: Missing arguments. - -#### Output on stdout - -- directory name of temporary directory created. - -#### Example - -```bash -echo "$(utility::make_temp_dir)" -#Output -tmp.rtfsxy -``` - -### file::name() - -Get only the filename from string path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- name of the file with extension. - -#### Example - -```bash -echo "$(file::name "/path/to/test.md")" -#Output -test.md -``` - -### file::basename() - -Get the basename of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- basename of the file. - -#### Example - -```bash -echo "$(file::basename "/path/to/test.md")" -#Output -test -``` - -### file::extension() - -Get the extension of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **1**: If no extension is found in the filename. -- **2**: Function missing arguments. - -#### Output on stdout - -- extension of the file. - -#### Example - -```bash -echo "$(file::extension "/path/to/test.md")" -#Output -md -``` - -### file::dirname() - -Get directory name from file path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- directory path. - -#### Example - -```bash -echo "$(file::dirname "/path/to/test.md")" -#Output -/path/to -``` - -### file::full_path() - -Get absolute path of file or directory. - -#### Arguments - -- **$1** (string): relative or absolute path to file/direcotry. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. - -#### Output on stdout - -- Absolute path to file/directory. - -#### Example - -```bash -file::full_path "../path/to/file.md" -#Output -/home/labbots/docs/path/to/file.md -``` - -### file::mime_type() - -Get mime type of provided input. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. -- **3**: If file or mimetype command not found in system. - -#### Output on stdout - -- mime type of file/directory. - -#### Example - -```bash -file::mime_type "../src/file.sh" -#Output -application/x-shellscript -``` - -### file::contains_text() - -Search if a given pattern is found in file. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. -- **$2** (string): search key or regular expression. - -#### Exit codes - -- **0**: If given search parameter is found in file. -- **1**: If search paramter not found in file. -- **2**: Function missing arguments. - -#### Example - -```bash -file::contains_text "./file.sh" "^[ @[:alpha:]]*" -file::contains_text "./file.sh" "@file" -#Output -0 -``` - -## Format - -Functions to format provided input. - -### format::human_readable_seconds() - -Format seconds to human readable format. - -#### Arguments - -- **$1** (int): number of seconds. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo "$(format::human_readable_seconds "356786")" -#Output -4 days 3 hours 6 minute(s) and 26 seconds -``` - -### format::bytes_to_human() - -Format bytes to human readable format. - -#### Arguments - -- **$1** (int): size in bytes. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted file size string. - -#### Example - -```bash -echo "$(format::bytes_to_human "2250")" -#Output -2.19 KB -``` - -### format::strip_ansi() - -Remove Ansi escape sequences from given text. - -#### Arguments - -- **$1** (string): Input text to be ansi stripped. - -#### Exit codes - -- **0**: If successful. - -#### Output on stdout - -- Ansi stripped text. - -#### Example - -```bash -format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -#Output -This is bold red text.This is green text. -``` - -### format::text_center() - -Prints the given text to centre of terminal. - -#### Arguments - -- **$1** (string): Text to be printed. -- **$2** (string): Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::text_center "This text is in centre of the terminal." "-" -``` - -### format::report() - -Format String to print beautiful report. - -#### Arguments - -- **$1** (string): Text to be printed on the left. -- **$2** (string): Text to be printed within the square brackets. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::report "Initialising mission state" "Success" -#Output -Initialising mission state ....................................................................[ Success ] -``` - -### format::trim_text_to_term() - -Trim given text to width of the terminal window. - -#### Arguments - -- **$1** (string): Text of first sentence. -- **$2** (string): Text of second sentence (optional). - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- trimmed text. - -#### Example - -```bash -format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -#Output -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -``` - -## Interaction - -Functions to enable interaction with the user. - -### interaction::prompt_yes_no() - -Prompt yes or no question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer \[yes/no\] (optional). - -#### Exit codes - -- **0**: If user responds with yes. -- **1**: If user responds with no. -- **2**: Function missing arguments. - -#### Example - -```bash -interaction::prompt_yes_no "Are you sure to proceed" "yes" -#Output -Are you sure to proceed (y/n)? [y] -``` - -### interaction::prompt_response() - -Prompt question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer (optional). - -#### Exit codes - -- **0**: If user responds with answer. -- **2**: Function missing arguments. - -#### Output on stdout - -- User entered answer to the question. - -#### Example - -```bash -interaction::prompt_response "Choose directory to install" "/home/path" -#Output -Choose directory to install? [/home/path] -``` - -## Json - -Simple json manipulation. These functions does not completely replace `jq` in any way. - -### json::get_value() - -Extract value from json based on key and position. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): id of the field to fetch. -- **$2** (int): position of value to extract.Defaults to 1.(optional) - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string value of extracted key. - -#### Example - -```bash -json::get_value "id" "1" < json_file -json::get_value "id" <<< "${json_var}" -echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -``` - -## Miscellaneous - -Set of miscellaneous helper functions. - -### misc::check_internet_connection() - -Check if internet connection is available. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script can connect to internet. -- **1**: If script cannot access internet. - -#### Example - -```bash -misc::check_internet_connection -``` - -### misc::get_pid() - -Get list of process ids based on process name. - -#### Arguments - -- **$1** (Name): of the process to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- list of process ids. - -#### Example - -```bash -misc::get_pid "chrome" -#Ouput -25951 -26043 -26528 -26561 -``` - -### misc::get_uid() - -Get user id based on username. - -#### Arguments - -- **$1** (username): to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string uid for the username. - -#### Example - -```bash -misc::get_uid "labbots" -#Ouput -1000 -``` - -### misc::generate_uuid() - -Generate random uuid. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If match successful. - -#### Output on stdout - -- random generated uuid. - -#### Example - -```bash -misc::generate_uuid -#Ouput -65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -``` - -## Operating System - -Functions to detect Operating system and version. - -### os::detect_os() - -Identify the OS the function is run on. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If OS is successfully detected. -- **1**: If unable to detect OS. - -#### Output on stdout - -- Operating system name (linux, mac or windows). - -#### Example - -```bash -os::detect_os -#Output -linux -``` - -### os::detect_linux_distro() - -Identify the distribution flavour of linux. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux distro is successfully detected. -- **1**: If unable to detect OS distro. - -#### Output on stdout - -- Linux OS distribution name (ubuntu, debian, suse, etc.,). - -#### Example - -```bash -os::detect_linux_distro -#Output -ubuntu -``` - -### os::detect_linux_version() - -Identify the Linux version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux version is successfully detected. -- **1**: If unable to detect Linux version. - -#### Output on stdout - -- Linux OS version number (18.04, 20.04, etc.,). - -#### Example - -```bash -os::detect_linux_version -#Output -20.04 -``` - -### os::detect_mac_version() - -Identify the MacOS version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If MacOS version is successfully detected. -- **1**: If unable to detect MacOS version. - -#### Output on stdout - -- MacOS version number (10.15.6, etc.,) - -#### Example - -```bash -os::detect_linux_version -#Output -10.15.7 -``` - -## String - -Functions for string operations and manipulations. - -### string::trim() - -Strip whitespace from the beginning and end of a string. - -#### Arguments - -- **$1** (string): The string to be trimmed. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The trimmed string. - -#### Example - -```bash -echo "$(string::trim " Hello World! ")" -#Output -Hello World! -``` - -### string::split() - -Split a string to array by a delimiter. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The delimiter string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns an array of strings created by splitting the string parameter by the delimiter. - -#### Example - -```bash -array=( $(string::split "a,b,c" ",") ) -printf "%s" "$(string::split "Hello!World" "!")" -#Output -Hello -World -``` - -### string::lstrip() - -Strip characters from the beginning of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::lstrip "Hello World!" "He")" -#Output -llo World! -``` - -### string::rstrip() - -Strip characters from the end of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::rstrip "Hello World!" "d!")" -#Output -Hello Worl -``` - -### string::to_lower() - -Make a string lowercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the lowercased string. - -#### Example - -```bash -echo "$(string::to_lower "HellO")" -#Output -hello -``` - -### string::to_upper() - -Make a string all uppercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the uppercased string. - -#### Example - -```bash -echo "$(string::to_upper "HellO")" -#Output -HELLO -``` - -### string::contains() - -Check whether the search string exists within the input string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::contains "Hello World!" "lo" -``` - -### string::starts_with() - -Check whether the input string starts with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::starts_with "Hello World!" "He" -``` - -### string::ends_with() - -Check whether the input string ends with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::ends_with "Hello World!" "d!" -``` - -### string::regex() - -Check whether the input string matches the given regex. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::regex "HELLO" "^[A-Z]*$" -``` - -## Terminal - -Set of useful terminal functions. - -### terminal::is_term() - -Check if script is run in terminal. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -### terminal::detect_profile() - -Detect profile rc file for zsh and bash of current script running user. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -#### Output on stdout - -- path to the profile file. - -### terminal::clear_line() - -Clear the output in terminal on the specified line number. -This function clears line only on terminal. - -#### Arguments - -- **$1** (Line): number to clear. Defaults to 1. (optional) - -#### Exit codes - -- **0**: If script is run on terminal. - -#### Output on stdout - -- clear line ansi code. - -## Validation - -Functions to perform validation on given data. - -### validation::email() - -Validate whether a given input is a valid email address or not. - -#### Arguments - -- **$1** (string): input email address to validate. - -#### Exit codes - -- **0**: If provided input is an email address. -- **1**: If provided input is not an email address. -- **2**: Function missing arguments. - -#### Example - -```bash -test='test@gmail.com' -validation::email "${test}" -echo $? -#Output -0 -``` - -### validation::ipv4() - -Validate whether a given input is a valid IP V4 address. - -#### Arguments - -- **$1** (string): input IPv4 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv4. -- **1**: If provided input is not a valid IPv4. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 4.2.2.2 - a.b.c.d - 192.168.1.1 - 0.0.0.0 - 255.255.255.255 - 255.255.255.256 - 192.168.0.1 - 192.168.0 - 1234.123.123.123 - 0.192.168.1 - ' -for ip in $ips; do - if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi - printf "%-20s: %s\n" "$ip" "$stat" -done -#Output -4.2.2.2 : good -a.b.c.d : bad -192.168.1.1 : good -0.0.0.0 : good -255.255.255.255 : good -255.255.255.256 : bad -192.168.0.1 : good -192.168.0 : bad -1234.123.123.123 : bad -0.192.168.1 : good -``` - -### validation::ipv6() - -Validate whether a given input is a valid IP V6 address. - -#### Arguments - -- **$1** (string): input IPv6 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv6. -- **1**: If provided input is not a valid IPv6. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 2001:db8:85a3:8d3:1319:8a2e:370:7348 - fe80::1ff:fe23:4567:890a - fe80::1ff:fe23:4567:890a%eth2 - 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar - fezy::1ff:fe23:4567:890a - :: - 2001:db8:: - ' -for ip in $ips; do - if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi - printf "%-50s= %s\n" "$ip" "$stat" -done -#Output -2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -fe80::1ff:fe23:4567:890a = good -fe80::1ff:fe23:4567:890a%eth2 = good -2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -fezy::1ff:fe23:4567:890a = bad -:: = good -2001:db8:: = good -``` - -### validation::alpha() - -Validate if given variable is entirely alphabetic characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is only alpha characters. -- **1**: If input contains any non alpha characters. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abcABC' -validation::alpha "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_num() - -Check if given variable contains only alpha-numeric characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is an alpha-numeric. -- **1**: If input is not an alpha-numeric. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc123' -validation::alpha_num "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_dash() - -Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is valid. -- **1**: If input the input is not valid. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc-ABC_cD' -validation::alpha_dash "${test}" -echo $? -#Output -0 -``` - -### validation::version_comparison() - -Compares version numbers and provides return based on whether the value in equal, less than or greater. - -#### Arguments - -- **$1** (string): Version number to check (eg: 1.0.1) - -#### Exit codes - -- **0**: version number is equal. -- **1**: $1 version number is greater than $2. -- **2**: $1 version number is less than $2. -- **3**: Function is missing required arguments. -- **4**: Provided input argument is in invalid format. - -#### Example - -```bash -test='abc-ABC_cD' -validation::version_comparison "12.0.1" "12.0.1" -echo $? -#Output -0 -``` - -## Variable - -Functions for handling variables. - -### variable::is_array() - -Check if given variable is array. -Pass the variable name instead of value of the variable. - -#### Arguments - -- **$1** (string): name of the variable to check. - -#### Exit codes - -- **0**: If input is array. -- **1**: If input is not an array. - -#### Example - -```bash -arr=("a" "b" "c") -variable::is_array "arr" -#Output -0 -``` - -### variable::is_numeric() - -Check if given variable is a number. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is number. -- **1**: If input is not a number. - -#### Example - -```bash -variable::is_numeric "1234" -#Output -0 -``` - -### variable::is_int() - -Check if given variable is an integer. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is an integer. -- **1**: If input is not an integer. - -#### Example - -```bash -variable::is_int "+1234" -#Output -0 -``` - -### variable::is_float() - -Check if given variable is a float. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a float. -- **1**: If input is not a float. - -#### Example - -```bash -variable::is_float "+1234.0" -#Output -0 -``` - -### variable::is_bool() - -Check if given variable is a boolean. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a boolean. -- **1**: If input is not a boolean. - -#### Example - -```bash -variable::is_bool "true" -#Output -0 -``` - -### variable::is_true() - -Check if given variable is a true. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is true. -- **1**: If input is not true. - -#### Example - -```bash -variable::is_true "true" -#Output -0 -``` - -### variable::is_false() - -Check if given variable is false. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is false. -- **1**: If input is not false. - -#### Example - -```bash -variable::is_false "false" -#Output -0 -``` - -### variable::is_empty_or_null() - -Check if given variable is empty or null. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is empty or null. -- **1**: If input is not empty or null. - -#### Example - -```bash -test='' -variable::is_empty_or_null $test -#Output -0 -``` - - - -## Inspired By - -- [Bash Bible](https://github.com/dylanaraps/pure-bash-bible) - A collection of pure bash alternatives to external processes. - -## License - -[MIT](https://github.com/labbots/google-drive-upload/blob/master/LICENSE) diff --git a/src/bash-utility/bash_utility.sh b/src/bash-utility/bash_utility.sh deleted file mode 100644 index 65411add0..000000000 --- a/src/bash-utility/bash_utility.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck disable=SC1091 -source src/array.sh -source src/string.sh -source src/variable.sh -source src/file.sh -source src/misc.sh -source src/date.sh -source src/interaction.sh -source src/check.sh -source src/format.sh -source src/collection.sh -source src/json.sh -source src/terminal.sh -source src/validation.sh -source src/debug.sh -source src/os.sh - - diff --git a/src/bash-utility/bin/bashdoc.awk b/src/bash-utility/bin/bashdoc.awk deleted file mode 100644 index 13efdaf7b..000000000 --- a/src/bash-utility/bin/bashdoc.awk +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/awk -f - -# Varibles -# style = readme or doc -# toc = true or false -BEGIN { - if (! style) { - style = "doc" - } - - if (! toc) { - toc = 0 - } - - styles["empty", "from"] = ".*" - styles["empty", "to"] = "" - - styles["h1", "from"] = ".*" - styles["h1", "to"] = "# &" - - styles["h2", "from"] = ".*" - styles["h2", "to"] = "## &" - - styles["h3", "from"] = ".*" - styles["h3", "to"] = "### &" - - styles["h4", "from"] = ".*" - styles["h4", "to"] = "#### &" - - styles["h5", "from"] = ".*" - styles["h5", "to"] = "##### &" - - styles["code", "from"] = ".*" - styles["code", "to"] = "```&" - - styles["/code", "to"] = "```" - - styles["argN", "from"] = "^(\\$[0-9]) (\\S+)" - styles["argN", "to"] = "**\\1** (\\2):" - - styles["arg@", "from"] = "^\\$@ (\\S+)" - styles["arg@", "to"] = "**...** (\\1):" - - styles["li", "from"] = ".*" - styles["li", "to"] = "- &" - - styles["i", "from"] = ".*" - styles["i", "to"] = "*&*" - - styles["anchor", "from"] = ".*" - styles["anchor", "to"] = "[&](#&)" - - styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" - styles["exitcode", "to"] = "**\\1**: \\2" - - styles["h_rule", "to"] = "---" - - styles["comment", "from"] = ".*" - styles["comment", "to"] = "" - - - output_format["readme", "h1"] = "h2" - output_format["readme", "h2"] = "h3" - output_format["readme", "h3"] = "h4" - output_format["readme", "h4"] = "h5" - - output_format["bashdoc", "h1"] = "h1" - output_format["bashdoc", "h2"] = "h2" - output_format["bashdoc", "h3"] = "h3" - output_format["bashdoc", "h4"] = "h4" - - output_format["webdoc", "h1"] = "empty" - output_format["webdoc", "h2"] = "h3" - output_format["webdoc", "h3"] = "h4" - output_format["webdoc", "h4"] = "h5" - -} - -function render(type, text) { - if((style,type) in output_format){ - type = output_format[style,type] - } - return gensub( \ - styles[type, "from"], - styles[type, "to"], - "g", - text \ - ) -} - -function render_list(item, anchor) { - return "- [" item "](#" anchor ")" -} - -function generate_anchor(text) { - # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 - text = tolower(text) - gsub(/[^[:alnum:]_ -]/, "", text) - gsub(/ /, "-", text) - return text -} - -function reset() { - has_example = 0 - has_args = 0 - has_exitcode = 0 - has_stdout = 0 - - content_desc = "" - content_example = "" - content_args = "" - content_exitcode = "" - content_seealso = "" - content_stdout = "" -} - -/^[[:space:]]*# @internal/ { - is_internal = 1 -} - -/^[[:space:]]*# @file/ { - sub(/^[[:space:]]*# @file /, "") - - filedoc = render("h1", $0) "\n" - if(style == "webdoc"){ - filedoc = filedoc render("comment", "file=" $0) "\n" - } - -} - -/^[[:space:]]*# @brief/ { - sub(/^[[:space:]]*# @brief /, "") - if(style == "webdoc"){ - filedoc = filedoc render("comment", "brief=" $0) "\n" - } - filedoc = filedoc "\n" $0 -} - -/^[[:space:]]*# @description/ { - in_description = 1 - in_example = 0 - - reset() - - docblock = "" -} - -in_description { - if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) { - if (!match(content_desc, /\n$/)) { - content_desc = content_desc "\n" - } - in_description = 0 - } else { - sub(/^[[:space:]]*# @description /, "") - sub(/^[[:space:]]*# /, "") - sub(/^[[:space:]]*#$/, "") - - content_desc = content_desc "\n" $0 - } -} - -in_example { - - if (! /^[[:space:]]*#[ ]{3}/) { - - in_example = 0 - - content_example = content_example "\n" render("/code") "\n" - } else { - sub(/^[[:space:]]*#[ ]{3}/, "") - - content_example = content_example "\n" $0 - } -} - -/^[[:space:]]*# @example/ { - in_example = 1 - content_example = content_example "\n" render("h3", "Example") - content_example = content_example "\n\n" render("code", "bash") -} - -/^[[:space:]]*# @arg/ { - if (!has_args) { - has_args = 1 - - content_args = content_args "\n" render("h3", "Arguments") "\n\n" - } - - sub(/^[[:space:]]*# @arg /, "") - - $0 = render("argN", $0) - $0 = render("arg@", $0) - - content_args = content_args render("li", $0) "\n" -} - -/^[[:space:]]*# @noargs/ { - content_args = content_args "\n" render("i", "Function has no arguments.") "\n" -} - -/^[[:space:]]*# @exitcode/ { - if (!has_exitcode) { - has_exitcode = 1 - - content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n" - } - - sub(/^[[:space:]]*# @exitcode /, "") - - $0 = render("exitcode", $0) - - content_exitcode = content_exitcode render("li", $0) "\n" -} - -/^[[:space:]]*# @see/ { - sub(/[[:space:]]*# @see /, "") - anchor = generate_anchor($0) - $0 = render_list($0, anchor) - - content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n" -} - -/^[[:space:]]*# @stdout/ { - has_stdout = 1 - - sub(/^[[:space:]]*# @stdout /, "") - - content_stdout = content_stdout "\n" render("h3", "Output on stdout") - content_stdout = content_stdout "\n\n" render("li", $0) "\n" -} - -{ - docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso - if(style == "webdoc"){ - docblock = docblock "\n" render("h_rule") "\n" - } -} - -/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example { - if (is_internal) { - is_internal = 0 - } else { - func_name = gensub(\ - /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \ - "\\3()", \ - "g" \ - ) - doc = doc "\n" render("h2", func_name) "\n" docblock - if (toc) { - url = generate_anchor(func_name) - - content_idx = content_idx "\n" "- [" func_name "](#" url ")" - } - } - - docblock = "" - reset() -} - -END { - if (filedoc != "") { - print filedoc - } - - if (toc) { - print "" - print render("h2", "Table of Contents") - print content_idx - print "" - print render("h_rule") - } - - print doc -} diff --git a/src/bash-utility/bin/generate_readme.sh b/src/bash-utility/bin/generate_readme.sh deleted file mode 100644 index 858a4b8be..000000000 --- a/src/bash-utility/bin/generate_readme.sh +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env bash - -#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh -#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh -_usage() { - printf " -Script to autogenerate markdown based on bash source code.\n -The script generates table of contents and bashdoc and update the given markdown file.\n -Usage:\n %s [options.. ]\n -Options:\n - -f | --file - Relative or absolute path to the README.md file. - -s | --sh-dir - path to the bash script source folder to generate shdocs.\n - -l | --toc-level - Minimum level of header to print in Table of Contents.\n - -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n - -w | --webdoc - Flag to indicate generation of webdoc.\n - -p | --dest-dir - Path in which wedoc files must be generated.\n - -h | --help - Display usage instructions.\n" "${0##*/}" - exit 0 -} - -_setup_arguments() { - - unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR - MINLEVEL=1 - MAXLEVEL=3 - SCRIPT_FILE="${0##*/}" - declare source="${BASH_SOURCE[0]}" - while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - source="$(readlink "$source")" - [[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located - done - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md" - SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src" - WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions" - - SHORTOPTS="whp:f:m:d:s:-:" - - while getopts "${SHORTOPTS}" OPTION; do - case "${OPTION}" in - -) - _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } - case "${OPTARG}" in - help) - _usage - ;; - file) - _check_longoptions "${!OPTIND}" - SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-level) - _check_longoptions "${!OPTIND}" - MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-depth) - _check_longoptions "${!OPTIND}" - MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - sh-dir) - _check_longoptions "${!OPTIND}" - SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - webdoc) - WEBDOC=true - ;; - dest-dir) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - '') - _usage - ;; - *) - printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - ;; - h) - _usage - ;; - f) - SOURCE_MARKDOWN="${OPTARG}" - ;; - m) - MINLEVEL="${OPTARG}" - ;; - d) - MAXLEVEL="${OPTARG}" - ;; - s) - SOURCE_SCRIPT_DIR="${OPTARG}" - ;; - w) - WEBDOC=true - ;; - p) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - :) - printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - ?) - printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - done - shift "$((OPTIND - 1))" - - if [[ -w "${SOURCE_MARKDOWN}" ]]; then - declare src_file src_extension - src_file="${SOURCE_MARKDOWN##*/}" - src_extension="${src_file##*.}" - if [[ "${src_extension,,}" != "md" ]]; then - printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 - fi - else - printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 - fi - - if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then - printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 - fi - - declare re='^[0-9]+$' - if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then - echo "error: Not a number" >&2 - exit 1 - fi - if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then - printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 - fi - - [ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}" - -} - -_setup_tempfile() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -_generate_shdoc() { - declare file - file="$(realpath "${1}")" - if [[ -s "${file}" ]]; then - awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2" - #awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md" - fi -} - -_insert_shdoc_to_file() { - declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc - shdoc_tmp_file="$1" - source_markdown="$2" - - start_shdoc="" - info_shdoc="" - end_shdoc="" - - sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then - # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file - - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" - echo -e "Updated bashdoc content to ${source_markdown} successfully\n" - - else - { - printf "%s\n" "${start_shdoc}" - cat "${shdoc_tmp_file}" - printf "%s\n" "${end_shdoc}" - } >> "${source_markdown}" - echo -e "Created bashdoc content to ${source_markdown} successfully\n" - fi -} - -_process_sh_files() { - declare shdoc_tmp_file source_script_dir source_markdown - source_markdown="${1}" - source_script_dir="${2}" - shdoc_tmp_file=$(_setup_tempfile) - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_shdoc "${line}" "${shdoc_tmp_file}" - done - _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" - rm "${shdoc_tmp_file}" - -} - -_generate_toc() { - - declare line level title anchor output counter temp_output invalid_chars - - invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" - while IFS='' read -r line || [[ -n "${line}" ]]; do - level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" - title="$(echo "${line}" | sed -E 's/^#+ //')" - [[ "${title}" = "Table of Contents" ]] && continue - - # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text - anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" - - # check new line introduced is not duplicated, if is duplicated, introduce a number at the end - temp_output=$output"$level- [$title](#$anchor)\n" - counter=1 - while true; do - nlines="$(echo -e "${temp_output}" | wc -l)" - duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)" - if [ "${nlines}" = "${duplines}" ]; then - break - fi - temp_output=$output"$level- [$title](#$anchor-$counter)\n" - counter=$((counter + 1)) - done - - output="$temp_output" - - # grep: filter header candidates to be included in toc - # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment - done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")" - - # when in toc we have two `--` quit one - echo "$output" - -} - -_insert_toc_to_file() { - - declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash - source_markdown="${1}" - toc_text="${2}" - start_toc="" - info_toc="" - end_toc="" - - toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" - # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command - utext_ampersand="id8234923000230gzz" - utext_slash="id9992384923423gzz" - toc_block="${toc_block//\&/${utext_ampersand}}" - toc_block="${toc_block//\//${utext_slash}}" - - # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 - # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches - if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then - # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace - sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" - echo -e "Updated TOC content in ${source_markdown} succesfully\n" - - else - sed -i 1i"$toc_block" "${source_markdown}" - echo -e "Created TOC in ${source_markdown} succesfully\n" - - fi - - # undo symbol replacements - sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" - sed -i "s,${utext_slash},\/,g" "${source_markdown}" - -} - -_process_toc() { - declare toc_temp_file source_markdown level toc_text - source_markdown="${1}" - - toc_temp_file=$(_setup_tempfile) - - sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" - - level=$MINLEVEL - while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do - level=$((level + 1)) - done - - MINLEVEL=${level} - toc_text=$(_generate_toc "${toc_temp_file}") - rm "${toc_temp_file}" - - _insert_toc_to_file "${source_markdown}" "${toc_text}" -} - -_generate_webdoc() { - declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file - declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc - declare webdoc_lastmod_date webdoc_lastmod_epoc - file="$(realpath "${1}")" - dest_dir="${2}" - - filename="${file##*/}" - file_basename="${filename%.*}" - dest_file_path="${dest_dir}/${file_basename}.md" - file_modified_date="$(date -r "${file}" +"%FT%T%:z")" - file_modified_date_epoc="$(date -r "${file}" +"%s")" - - start_shdoc="" - end_shdoc="" - if [[ ! -f "${dest_file_path}" ]]; then - - cat << EOF > "${dest_file_path}" ---- -title : -description : -date : ${file_modified_date} -lastmod : ${file_modified_date} ---- -${start_shdoc} -${end_shdoc} -EOF - is_new_file=true - else - is_new_file=false - webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")" - webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")" - fi - - if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then - - shdoc_tmp_file=$(_setup_tempfile) - if [[ -s "${file}" ]]; then - awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}" - fi - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}" - fi - - # Extract title and description from webdoc - title="$(sed -ne 's/-->//; s/^.*//; s/^.*/${title}/g" "${dest_file_path}" - sed -i -e "s//${description}/g" "${dest_file_path}" - else - sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}" - sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}" - - fi - - # Update the last modified timestamp in front matter - sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}" - - echo -e "Updated bashdoc content to ${dest_file_path} successfully." - rm "${shdoc_tmp_file}" - - fi -} -_process_webdoc_files() { - declare source_script_dir dest_dir - - source_script_dir="${1}" - dest_dir="${2}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_webdoc "${line}" "${dest_dir}" - done -} - -_count_library_functions() { - declare source_script_dir - source_script_dir="${1}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - { - declare -i function_count=0 count=0 - while IFS= read -r -d '' line; do - count=0 - count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}") - function_count=$((function_count + count)) - done - printf "Total library functions: %s \n" "${function_count}" - - } -} -main() { - # export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' - # set -x - trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT - set -o errexit -o noclobber -o pipefail - - _setup_arguments "${@}" - _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" - _process_toc "${SOURCE_MARKDOWN}" - - if [[ -n ${WEBDOC} ]]; then - _process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}" - fi - _count_library_functions "${SOURCE_SCRIPT_DIR}" -} - -main "${@}" diff --git a/src/bash-utility/image/bash-utility.png b/src/bash-utility/image/bash-utility.png deleted file mode 100644 index c1bfb8b556809ce645cf41ab6d4b11547df68fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32396 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 diff --git a/src/bash-utility/src/array.sh b/src/bash-utility/src/array.sh deleted file mode 100644 index 42d5883b8..000000000 --- a/src/bash-utility/src/array.sh +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env bash - -# @file Array -# @brief Functions for array operations and manipulations. - -# @description Check if item exists in the given array. -# -# @example -# array=("a" "b" "c") -# array::contains "c" ${array[@]} -# #Output -# 0 -# -# @arg $1 mixed Item to search (needle). -# @arg $2 array array to be searched (haystack). -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found in the array. -# @exitcode 2 Function missing arguments. -array::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare query="${1:-}" - shift - - for element in "${@}"; do - [[ "${element}" == "${query}" ]] && return 0 - done - - return 1 -} - -# @description Remove duplicate items from the array. -# -# @example -# array=("a" "b" "a" "c") -# printf "%s" "$(array::dedupe ${array[@]})" -# #Output -# a -# b -# c -# -# @arg $1 array Array to be deduped. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Deduplicated array. -array::dedupe() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -A arr_tmp - declare -a arr_unique - for i in "$@"; do - { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue - arr_unique+=("${i}") && arr_tmp[${i}]=x - done - printf '%s\n' "${arr_unique[@]}" -} - -# @description Check if a given array is empty. -# -# @example -# array=("a" "b" "c" "d") -# array::is_empty "${array[@]}" -# -# @arg $1 array Array to be checked. -# -# @exitcode 0 If the given array is empty. -# @exitcode 2 If the given array is not empty. -array::is_empty() { - declare -a array - local array=("$@") - if [ ${#array[@]} -eq 0 ]; then - return 0 - else - return 1 - fi -} -# @description Join array elements with a string. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s" "$(array::join "," "${array[@]}")" -# #Output -# a,b,c,d -# printf "%s" "$(array::join "" "${array[@]}")" -# #Output -# abcd -# -# @arg $1 string String to join the array elements (glue). -# @arg $2 array array to be joined with glue string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. -array::join() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare delimiter="${1}" - shift - printf "%s" "${1}" - shift - printf "%s" "${@/#/${delimiter}}" -} - -# @description Return an array with elements in reverse order. -# -# @example -# array=(1 2 3 4 5) -# printf "%s" "$(array::reverse "${array[@]}")" -# #Output -# 5 4 3 2 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The reversed array. -array::reverse() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare min=0 - declare -a array - array=("$@") - declare max=$((${#array[@]} - 1)) - - while [[ $min -lt $max ]]; do - # Swap current first and last elements - x="${array[$min]}" - array[$min]="${array[$max]}" - array[$max]="$x" - - # Move closer - ((min++, max--)) - done - printf '%s\n' "${array[@]}" -} - -# @description Returns a random item from the array. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s\n" "$(array::random_element "${array[@]}")" -# #Output -# c -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Random item out of the array. -array::random_element() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array - local array=("$@") - printf '%s\n' "${array[RANDOM % $#]}" -} - -# @description Sort an array from lowest to highest. -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# 1 -# 2 -# 4 5 -# a -# a c -# d -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout sorted array. -array::sort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort <<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Sort an array in reverse order (highest to lowest). -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# d -# a c -# a -# 4 5 -# 2 -# 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout reverse sorted array. -array::rsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort -r<<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Bubble sort an integer array from lowest to highest. -# This sort does not work on string array. -# @example -# iarr=(4 5 1 3) -# array::bsort "${iarr[@]}" -# #Output -# 1 -# 3 -# 4 -# 5 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout bubble sorted array. -array::bsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare tmp - declare arr=("$@") - for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do - for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do - if [[ ${arr[i]} -gt ${arr[j]} ]]; then - # echo $i $j ${arr[i]} ${arr[j]} - tmp=${arr[i]} - arr[i]=${arr[j]} - arr[j]=$tmp - fi - done - done - printf "%s\n" "${arr[@]}" -} - -# @description Merge two arrays. -# Pass the variable name of the array instead of value of the variable. -# @example -# a=("a" "c") -# b=("d" "c") -# array::merge "a[@]" "b[@]" -# #Output -# a -# c -# d -# c -# -# @arg $1 string variable name of first array. -# @arg $2 string variable name of second array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Merged array. -array::merge() { - [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr1=("${!1}") - declare -a arr2=("${!2}") - declare out=("${arr1[@]}" "${arr2[@]}") - printf "%s\n" "${out[@]}" -} diff --git a/src/bash-utility/src/check.sh b/src/bash-utility/src/check.sh deleted file mode 100644 index 2b7c1eb1d..000000000 --- a/src/bash-utility/src/check.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# @file Check -# @brief Helper functions. - -# @description Check if the command exists in the system. -# -# @example -# check::command_exists "tput" -# -# @arg $1 string Command name to be searched. -# -# @exitcode 0 If the command exists. -# @exitcode 1 If the command does not exist. -# @exitcode 2 Function missing arguments. -check::command_exists() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - hash "${1}" 2> /dev/null -} - -# @description Check if the script is executed with sudo privilege. -# -# @example -# check::is_sudo -# -# @noargs -# -# @exitcode 0 If the script is executed with root privilege. -# @exitcode 1 If the script is not executed with root privilege -check::is_sudo() { - if [[ $(id -u) -ne 0 ]]; then - return 1 - fi -} diff --git a/src/bash-utility/src/collection.sh b/src/bash-utility/src/collection.sh deleted file mode 100644 index 9f0e244a2..000000000 --- a/src/bash-utility/src/collection.sh +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env bash - -# @file Collection -# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -# @description Iterates over elements of collection and invokes iteratee for each element. -# Input to the function can be a pipe output, here-string or file. -# @example -# test_func(){ -# printf "print value: %s\n" "$1" -# return 0 -# } -# arr1=("a b" "c d" "a" "d") -# printf "%s\n" "${arr1[@]}" | collection::each "test_func" -# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -# #output -# print value: a b -# print value: c d -# print value: a -# print value: d -# -# @example -# # If other function from this library is already used to process the array. -# # Then following method could be used to pass the array to the function. -# out=("$(array::dedupe "${arr1[@]}")") -# collection::each "test_func" <<< "${out[@]}" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output of iteratee function. -collection::each() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - done -} - -# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "4") -# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If iteratee function fails. -# @exitcode 2 Function missing arguments. -collection::every() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return 1 - fi - - done -} - -# @description Iterates over elements of array, returning all elements where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -# #output -# 1 -# 2 -# 3 -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values matching the iteratee function. -collection::filter() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s\n" "${it}" - fi - done -} - -# @description Iterates over elements of collection, returning the first element where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("1" "2" "3" "a") -# check_a(){ -# [[ "$1" = "a" ]] -# } -# printf "%s\n" "${arr[@]}" | collection::find "check_a" -# #Output -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -# -# @stdout first array value matching the iteratee function. -collection::find() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s" "${it}" - return 0 - fi - done - - return 1 -} - -# @description Invokes the iteratee with each element passed as argument to the iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# opt=("-a" "-l") -# printf "%s\n" "${opt[@]}" | collection::invoke "ls" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output from the iteratee function. -collection::invoke() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a args=() - declare func="${1}" - while read -r it; do - args=("${args[@]}" "$it") - done - - eval "${func}" "${args[@]}" -} - -# @description Creates an array of values by running each element in array through iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3") -# add_one(){ -# i=${1} -# i=$(( i + 1 )) -# printf "%s\n" "$i" -# } -# printf "%s\n" "${arri[@]}" | collection::map "add_one" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output result of iteratee on value. -collection::map() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - declare out - - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - out="$("${func}")" - else - out="$("${func}" "$it")" - fi - - declare -i ret=$? - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - printf "%s\n" "${out}" - done -} - -# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -# #Ouput -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values not matching the iteratee function. -# @see collection::filter -collection::reject() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - echo "$it" - fi - - done -} - -# @description Checks if iteratee returns true for any element of the array. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("a" "b" "3" "a") -# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If match successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -collection::some() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - - declare -i ret=$? - - if [[ $ret -eq 0 ]]; then - return 0 - fi - done - - return 1 -} diff --git a/src/bash-utility/src/date.sh b/src/bash-utility/src/date.sh deleted file mode 100644 index 41f071792..000000000 --- a/src/bash-utility/src/date.sh +++ /dev/null @@ -1,744 +0,0 @@ -#!/usr/bin/env bash - -# @file Date -# @brief Functions for manipulating dates. - -# @description Get current time in unix timestamp. -# -# @example -# echo "$(date::now)" -# #Output -# 1591554426 -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout current timestamp. -date::now() { - declare now - now="$(date --universal +%s)" || return $? - printf "%s" "${now}" -} - -# @description convert datetime string to unix timestamp. -# -# @example -# echo "$(date::epoc "2020-07-07 18:38")" -# #Output -# 1594143480 -# -# @arg $1 string date time in any format. -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp for specified datetime. -date::epoc() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare date - date=$(date -d "${1}" +"%s") || return $? - printf "%s" "${date}" -} - -# @description Add number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days_from "1594143480")" -# #Output -# 1594229880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months_from "1594143480")" -# #Output -# 1596821880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years_from "1594143480")" -# #Output -# 1625679480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::add_weeks_from "1594143480")" -# #Output -# 1594748280 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours_from "1594143480")" -# #Output -# 1594147080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes_from "1594143480")" -# #Output -# 1594143540 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds_from "1594143480")" -# #Output -# 1594143481 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days "1")" -# #Output -# 1591640826 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months "1")" -# #Output -# 1594146426 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years "1")" -# #Output -# 1623090426 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_weeks "1")" -# #Output -# 1592159226 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours "1")" -# #Output -# 1591558026 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes "1")" -# #Output -# 1591554486 -# -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds "1")" -# #Output -# 1591554427 -# -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days_from "1594143480")" -# #Output -# 1594057080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months_from "1594143480")" -# #Output -# 1591551480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years_from "1594143480")" -# #Output -# 1562521080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks_from "1594143480")" -# #Output -# 1593538680 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours_from "1594143480")" -# #Output -# 1594139880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes_from "1594143480")" -# #Output -# 1594143420 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds_from "1594143480")" -# #Output -# 1594143479 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days "1")" -# #Output -# 1588876026 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months "1")" -# #Output -# 1559932026 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years "1")" -# #Output -# 1591468026 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks "1")" -# #Output -# 1590949626 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours "1")" -# #Output -# 1591550826 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes "1")" -# #Output -# 1591554366 -# -# @arg $1 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds "1")" -# #Output -# 1591554425 -# -# @arg $1 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Format unix timestamp to human readable format. -# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. -# -# @example -# echo echo "$(date::format "1594143480")" -# #Output -# 2020-07-07 18:38:00 -# -# @arg $1 int unix timestamp. -# @arg $2 string format control characters based on `date` command (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate time string. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -date::format() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp format out - timestamp="${1}" - format="${2:-"%F %T"}" - out="$(date -d "@${timestamp}" +"${format}")" || return $? - printf "%s" "${out}" - -} diff --git a/src/bash-utility/src/debug.sh b/src/bash-utility/src/debug.sh deleted file mode 100644 index 82828734f..000000000 --- a/src/bash-utility/src/debug.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash - -# @file Debug -# @brief Functions to facilitate debugging scripts. - -# @description Prints the content of array as key value pair for easier debugging. -# Pass the variable name of the array instead of value of the variable. -# @example -# array=(foo bar baz) -# printf "Array\n" -# printarr "array" -# declare -A assoc_array -# assoc_array=([foo]=bar [baz]=foobar) -# printf "Assoc Array\n" -# printarr "assoc_array" -# #Output -# Array -# 0 = foo -# 1 = bar -# 2 = baz -# Assoc Array -# baz = foobar -# foo = bar -# -# @arg $1 string variable name of the array. -# -# @stdout Formatted key value of array. -debug::print_array() { - declare -n __arr="$1" - for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done -} - -# @description Function to print ansi escape sequence as is. -# This function helps debug ansi escape sequence in text by displaying the escape codes. -# -# @example -# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -# debug::print_ansi "$txt" -# #Output -# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -# -# @arg $1 string input with ansi escape sequence. -# -# @stdout Ansi escape sequence printed in output as is. -debug::print_ansi() { - #echo $(tr -dc '[:print:]'<<<$1) - printf "%s\n" "${1//$'\e'/\\e}" - -} diff --git a/src/bash-utility/src/file.sh b/src/bash-utility/src/file.sh deleted file mode 100644 index 71afd8476..000000000 --- a/src/bash-utility/src/file.sh +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env bash - -# @file File -# @brief Functions for handling files. - -# @description Create temporary file. -# Function creates temporary file with random name. The temporary file will be deleted when script finishes. -# -# @example -# echo "$(file::make_temp_file)" -# #Output -# tmp.vgftzy -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp file. -# -# @stdout file name of temporary file created. -file::make_temp_file() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -# @description Create temporary directory. -# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. -# -# @example -# echo "$(utility::make_temp_dir)" -# #Output -# tmp.rtfsxy -# -# @arg $1 string Temporary directory prefix -# @arg $2 string Flag to auto remove directory on exit trap (true) -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp directory. -# @exitcode 2 Missing arguments. -# -# @stdout directory name of temporary directory created. -file::make_temp_dir() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare temp_dir prefix="${1}" trap_rm="${2}" - temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") - if [[ -n "${trap_rm}" ]]; then - trap 'rm -rf "${temp_dir}"' EXIT - fi - printf "%s" "${temp_dir}" -} - -# @description Get only the filename from string path. -# -# @example -# echo "$(file::name "/path/to/test.md")" -# #Output -# test.md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout name of the file with extension. -file::name() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf "%s" "${1##*/}" -} - -# @description Get the basename of file from file name. -# -# @example -# echo "$(file::basename "/path/to/test.md")" -# #Output -# test -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout basename of the file. -file::basename() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare file basename - file="${1##*/}" - basename="${file%.*}" - - printf "%s" "${basename}" -} - -# @description Get the extension of file from file name. -# -# @example -# echo "$(file::extension "/path/to/test.md")" -# #Output -# md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 1 If no extension is found in the filename. -# @exitcode 2 Function missing arguments. -# -# @stdout extension of the file. -file::extension() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare file extension - file="${1##*/}" - extension="${file##*.}" - [[ "${file}" = "${extension}" ]] && return 1 - - printf "%s" "${extension}" -} - -# @description Get directory name from file path. -# -# @example -# echo "$(file::dirname "/path/to/test.md")" -# #Output -# /path/to -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout directory path. -file::dirname() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare tmp=${1:-.} - - [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } - tmp="${tmp%%"${tmp##*[!/]}"}" - - [[ ${tmp} != */* ]] && { printf '.\n' && return; } - tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" - - printf '%s' "${tmp:-/}" -} - -# @description Get absolute path of file or directory. -# -# @example -# file::full_path "../path/to/file.md" -# #Output -# /home/labbots/docs/path/to/file.md -# -# @arg $1 string relative or absolute path to file/direcotry. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# -# @stdout Absolute path to file/directory. -file::full_path() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare input="${1}" - if [[ -f ${input} ]]; then - printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" - elif [[ -d ${input} ]]; then - printf "%s\n" "$(cd "${input}" && pwd)" - else - return 1 - fi -} - -# @description Get mime type of provided input. -# -# @example -# file::mime_type "../src/file.sh" -# #Output -# application/x-shellscript -# -# @arg $1 string relative or absolute path to file/directory. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# @exitcode 3 If file or mimetype command not found in system. -# -# @stdout mime type of file/directory. -file::mime_type() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare mime_type - if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then - if type -p mimetype &> /dev/null; then - mime_type=$(mimetype --output-format %m "${1}") - elif type -p file &> /dev/null; then - mime_type=$(file --brief --mime-type "${1}") - else - return 3 - fi - else - return 1 - fi - printf "%s" "${mime_type}" -} - -# @description Search if a given pattern is found in file. -# -# @example -# file::contains_text "./file.sh" "^[ @[:alpha:]]*" -# file::contains_text "./file.sh" "@file" -# #Output -# 0 -# -# @arg $1 string relative or absolute path to file/directory. -# @arg $2 string search key or regular expression. -# -# @exitcode 0 If given search parameter is found in file. -# @exitcode 1 If search paramter not found in file. -# @exitcode 2 Function missing arguments. -file::contains_text() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - declare -r file="$1" - declare -r text="$2" - grep -q "$text" "$file" -} diff --git a/src/bash-utility/src/format.sh b/src/bash-utility/src/format.sh deleted file mode 100644 index 7dceb1d59..000000000 --- a/src/bash-utility/src/format.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash - -# @file Format -# @brief Functions to format provided input. - -# @internal -# @description Initialisation script when the code is sourced. -# -# @noargs -__init(){ -_check_terminal_window_size -} - -# @internal -# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. -# -# @noargs -_check_terminal_window_size() { - shopt -s checkwinsize && (: && :) - trap 'shopt -s checkwinsize; (:;:)' SIGWINCH -} -# @description Format seconds to human readable format. -# -# @example -# echo "$(format::human_readable_seconds "356786")" -# #Output -# 4 days 3 hours 6 minute(s) and 26 seconds -# -# @arg $1 int number of seconds. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -format::human_readable_seconds() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare T="${1}" - declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" - [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" - [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" - [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" - [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' - printf '%d seconds\n' "${SEC}" -} - -# @description Format bytes to human readable format. -# -# @example -# echo "$(format::bytes_to_human "2250")" -# #Output -# 2.19 KB -# -# @arg $1 int size in bytes. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted file size string. -format::bytes_to_human() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) - while ((b > 1024)); do - d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" - b=$((b / 1024)) && ((s++)) - done - printf "%s\n" "${b}${d} ${S[${s}]}" -} - -# @description Remove Ansi escape sequences from given text. -# -# @example -# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -# #Output -# This is bold red text.This is green text. -# -# @arg $1 string Input text to be ansi stripped. -# -# @exitcode 0 If successful. -# -# @stdout Ansi stripped text. -format::strip_ansi() { - declare tmp esc tpa re - tmp="${1}" - esc=$(printf "\x1b") - tpa=$(printf "\x28") - re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" - while [[ "${tmp}" =~ $re ]]; do - tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" - done - printf "%s" "${tmp}" -} - -# @description Prints the given text to centre of terminal. -# -# @example -# format::text_center "This text is in centre of the terminal." "-" -# -# @arg $1 string Text to be printed. -# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::text_center() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare input="${1}" symbol="${2:- }" filler out no_ansi_out - no_ansi_out=$(format::strip_ansi "$input") - declare -i str_len=${#no_ansi_out} - declare -i filler_len="$(((COLUMNS - str_len) / 2))" - - [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" - for ((i = 0; i < filler_len; i++)); do - filler+="${symbol}" - done - - out="${filler}${input}${filler}" - [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" - printf "%s" "${out}" -} - -# @description Format String to print beautiful report. -# -# @example -# format::report "Initialising mission state" "Success" -# #Output -# Initialising mission state ....................................................................[ Success ] -# -# @arg $1 string Text to be printed on the left. -# @arg $2 string Text to be printed within the square brackets. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::report() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare symbol="." to_print y hl hlout out - declare input1="${1} " input2="${2}" - input2="[ $input2 ]" - to_print="$((COLUMNS * 60 / 100))" - y=$(( to_print - ( ${#input1} + ${#input2} ) )) - hl="$(printf '%*s' $y '')" - hlout=${hl// /${symbol}} - out="${input1}${hlout}${input2}" - printf "%s\n" "${out}" -} - -# @description Trim given text to width of the terminal window. -# -# @example -# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -# #Output -# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -# -# @arg $1 string Text of first sentence. -# @arg $2 string Text of second sentence (optional). -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout trimmed text. -format::trim_text_to_term() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare to_print out input1="$1" input2="$2" - if [[ $# = 1 ]]; then - to_print="$((COLUMNS * 93 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } - else - to_print="$((COLUMNS * 40 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } - to_print="$((COLUMNS * 53 / 100))" - { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } - fi - printf "%s" "$out" -} - -__init diff --git a/src/bash-utility/src/interaction.sh b/src/bash-utility/src/interaction.sh deleted file mode 100644 index 910b60e1c..000000000 --- a/src/bash-utility/src/interaction.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# @file Interaction -# @brief Functions to enable interaction with the user. - -# @description Prompt yes or no question to the user. -# -# @example -# interaction::prompt_yes_no "Are you sure to proceed" "yes" -# #Output -# Are you sure to proceed (y/n)? [y] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer \[yes/no\] (optional). -# -# @exitcode 0 If user responds with yes. -# @exitcode 1 If user responds with no. -# @exitcode 2 Function missing arguments. -interaction::prompt_yes_no() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare def_arg response - def_arg="" - response="" - - case "${2}" in - [yY] | [yY][eE][sS]) - def_arg=y - ;; - [nN] | [nN][oO]) - def_arg=n - ;; - esac - - while :; do - printf "%s (y/n)? " "${1}" - [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -z "${response}" ]] && response="${def_arg}" - - case "${response}" in - [yY] | [yY][eE][sS]) - response=y - break - ;; - [nN] | [nN][oO]) - response=n - break - ;; - *) - response="" - ;; - esac - done - - [[ "${response}" = 'y' ]] && return 0 || return 1 -} - -# @description Prompt question to the user. -# -# @example -# interaction::prompt_response "Choose directory to install" "/home/path" -# #Output -# Choose directory to install? [/home/path] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer (optional). -# -# @exitcode 0 If user responds with answer. -# @exitcode 2 Function missing arguments. -# -# @stdout User entered answer to the question. -interaction::prompt_response() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare def_arg response - response="" - def_arg="${2}" - - while :; do - printf "%s ? " "${1}" - [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -n "${response}" ]] && break - - if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then - response="${def_arg}" - break - fi - done - - [[ "${response}" = "-" ]] && response="" - - printf "%s" "${response}" -} diff --git a/src/bash-utility/src/json.sh b/src/bash-utility/src/json.sh deleted file mode 100644 index 73476618e..000000000 --- a/src/bash-utility/src/json.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# @file Json -# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. - -# @description Extract value from json based on key and position. -# Input to the function can be a pipe output, here-string or file. -# @example -# json::get_value "id" "1" < json_file -# json::get_value "id" <<< "${json_var}" -# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -# -# @arg $1 string id of the field to fetch. -# @arg $2 int position of value to extract.Defaults to 1.(optional) -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string value of extracted key. -json::get_value() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare LC_ALL=C num="${2:-1}" - grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p -} diff --git a/src/bash-utility/src/misc.sh b/src/bash-utility/src/misc.sh deleted file mode 100644 index fca9a75bf..000000000 --- a/src/bash-utility/src/misc.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash - -# @file Miscellaneous -# @brief Set of miscellaneous helper functions. - -# @internal -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -_is_terminal() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Check if internet connection is available. -# -# @example -# misc::check_internet_connection -# -# @noargs -# -# @exitcode 0 If script can connect to internet. -# @exitcode 1 If script cannot access internet. -misc::check_internet_connection() { - declare check_internet - if _is_terminal; then - check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" - else - check_internet="$(curl --compressed -Is google.com -m 10)" - fi - if [[ -z ${check_internet} ]]; then - return 1 - fi -} - -# @description Get list of process ids based on process name. -# -# @example -# misc::get_pid "chrome" -# #Ouput -# 25951 -# 26043 -# 26528 -# 26561 -# -# @arg $1 Name of the process to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout list of process ids. -misc::get_pid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - pgrep "${1}" -} - -# @description Get user id based on username. -# -# @example -# misc::get_uid "labbots" -# #Ouput -# 1000 -# -# @arg $1 username to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string uid for the username. -misc::get_uid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - user_id=$(id "${1}" 2> /dev/null) - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - printf "No user found with username: %s" "${1}\n" - return 1 - fi - - printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' - - unset user_id -} - -# @description Generate random uuid. -# -# @example -# misc::generate_uuid -# #Ouput -# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -# -# @noargs -# -# @exitcode 0 If match successful. -# -# @stdout random generated uuid. -misc::generate_uuid() { - C="89ab" - - for ((N=0;N<16;++N)); do - B="$((RANDOM%256))" - - case "$N" in - 6) printf '4%x' "$((B%16))" ;; - 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; - - 3|5|7|9) - printf '%02x-' "$B" - ;; - - *) - printf '%02x' "$B" - ;; - esac - done - - printf '\n' -} diff --git a/src/bash-utility/src/os.sh b/src/bash-utility/src/os.sh deleted file mode 100644 index 5cfecfc78..000000000 --- a/src/bash-utility/src/os.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash - -# @file Operating System -# @brief Functions to detect Operating system and version. - -# @description Identify the OS the function is run on. -# -# @noargs -# -# @example -# os::detect_os -# #Output -# linux -# -# @exitcode 0 If OS is successfully detected. -# @exitcode 1 If unable to detect OS. -# -# @stdout Operating system name (linux, mac or windows). -os::detect_os() { - declare uname os - uname=$(command -v uname) - - case $("${uname}" | tr '[:upper:]' '[:lower:]') in - linux*) - os="linux" - ;; - darwin*) - os="mac" - ;; - msys* | cygwin* | mingw* | nt | win*) - # or possible 'bash on windows' - os="windows" - ;; - *) - return 1 - ;; - esac - printf "%s" "${os}" -} - -# @description Identify the distribution flavour of linux. -# -# @noargs -# -# @example -# os::detect_linux_distro -# #Output -# ubuntu -# @exitcode 0 If Linux distro is successfully detected. -# @exitcode 1 If unable to detect OS distro. -# -# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). -os::detect_linux_distro() { - declare distro - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro="${NAME}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro=$(lsb_release -si) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro="${DISTRIB_ID}" - elif [[ -f /etc/debian_version ]]; then - # Older Debian/Ubuntu/etc. - distro="debian" - elif [[ -f /etc/SuSe-release ]]; then - # Older SuSE/etc. - distro="suse" - elif [[ -f /etc/redhat-release ]]; then - # Older Red Hat, CentOS, etc. - distro="redhat" - else - return 1 - fi - printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' -} - -# @description Identify the Linux version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 20.04 -# -# @exitcode 0 If Linux version is successfully detected. -# @exitcode 1 If unable to detect Linux version. -# -# @stdout Linux OS version number (18.04, 20.04, etc.,). -os::detect_linux_version() { - declare distro_version - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro_version="${VERSION_ID}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro_version=$(lsb_release -sr) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro_version="${DISTRIB_RELEASE}" - else - return 1 - fi - printf "%s" "${distro_version}" -} - -# @description Identify the MacOS version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 10.15.7 -# @exitcode 0 If MacOS version is successfully detected. -# @exitcode 1 If unable to detect MacOS version. -# -# @stdout MacOS version number (10.15.6, etc.,) -os::detect_mac_version() { - if [[ "$(os::detect_os)" = "mac" ]]; then - declare mac_version - mac_version="$(sw_vers -productVersion)" - printf "%s" "${mac_version}" - else - return 1 - fi -} diff --git a/src/bash-utility/src/string.sh b/src/bash-utility/src/string.sh deleted file mode 100644 index aa522cb55..000000000 --- a/src/bash-utility/src/string.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env bash - -# @file String -# @brief Functions for string operations and manipulations. - -# @description Strip whitespace from the beginning and end of a string. -# -# @example -# echo "$(string::trim " Hello World! ")" -# #Output -# Hello World! -# -# @arg $1 string The string to be trimmed. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The trimmed string. -string::trim() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - : "${1#"${1%%[![:space:]]*}"}" - : "${_%"${_##*[![:space:]]}"}" - printf '%s\n' "$_" -} - -# @description Split a string to array by a delimiter. -# -# @example -# array=( $(string::split "a,b,c" ",") ) -# printf "%s" "$(string::split "Hello!World" "!")" -# #Output -# Hello -# World -# -# @arg $1 string The input string. -# @arg $2 string The delimiter string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. -string::split() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr=() - IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" - printf '%s\n' "${arr[@]}" -} - -# @description Strip characters from the beginning of a string. -# -# @example -# echo "$(string::lstrip "Hello World!" "He")" -# #Output -# llo World! -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::lstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1##$2}" -} - -# @description Strip characters from the end of a string. -# -# @example -# echo "$(string::rstrip "Hello World!" "d!")" -# #Output -# Hello Worl -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::rstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1%%$2}" -} - -# @description Make a string lowercase. -# -# @example -# echo "$(string::to_lower "HellO")" -# #Output -# hello -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the lowercased string. -string::to_lower() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1,,}" - else - printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' - fi -} - -# @description Make a string all uppercase. -# -# @example -# echo "$(string::to_upper "HellO")" -# #Output -# HELLO -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the uppercased string. -string::to_upper() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1^^}" - else - printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' - fi -} - -# @description Check whether the search string exists within the input string. -# -# @example -# string::contains "Hello World!" "lo" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_contains hello he - [[ "${1}" == *${2}* ]] -} - -# @description Check whether the input string starts with key string. -# -# @example -# string::starts_with "Hello World!" "He" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::starts_with() { - # Usage: string_starts_with hello he - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - [[ "${1}" == ${2}* ]] -} - -# @description Check whether the input string ends with key string. -# -# @example -# string::ends_with "Hello World!" "d!" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::ends_with() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_ends_wit hello lo - [[ "${1}" == *${2} ]] -} - -# @description Check whether the input string matches the given regex. -# -# @example -# string::regex "HELLO" "^[A-Z]*$" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::regex() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${1} =~ ${2} ]]; then - return 0 - else - return 1 - fi - -} diff --git a/src/bash-utility/src/terminal.sh b/src/bash-utility/src/terminal.sh deleted file mode 100644 index d73331d7a..000000000 --- a/src/bash-utility/src/terminal.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# @file Terminal -# @brief Set of useful terminal functions. - -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -terminal::is_term() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Detect profile rc file for zsh and bash of current script running user. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -# -# @stdout path to the profile file. -terminal::detect_profile() { - declare CURRENT_SHELL="${SHELL##*/}" - case "${CURRENT_SHELL}" in - 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; - 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; - *) if [[ -f "${HOME}/.profile" ]]; then - DETECTED_PROFILE="${HOME}/.profile" - else - printf "No compaitable shell file\n" && exit 1 - fi ;; - esac - printf "%s\n" "${DETECTED_PROFILE}" -} - -# @description Clear the output in terminal on the specified line number. -# This function clears line only on terminal. -# -# @arg $1 Line number to clear. Defaults to 1. (optional) -# -# @exitcode 0 If script is run on terminal. -# -# @stdout clear line ansi code. -terminal::clear_line() { - if terminal::is_term; then - declare line=${1:-1} - printf "\033[%sA\033[2K" "${line}" - fi -} diff --git a/src/bash-utility/src/validation.sh b/src/bash-utility/src/validation.sh deleted file mode 100644 index 37fde1cbc..000000000 --- a/src/bash-utility/src/validation.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env bash - -# @file Validation -# @brief Functions to perform validation on given data. - -# @description Validate whether a given input is a valid email address or not. -# -# @example -# test='test@gmail.com' -# validation::email "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string input email address to validate. -# -# @exitcode 0 If provided input is an email address. -# @exitcode 1 If provided input is not an email address. -# @exitcode 2 Function missing arguments. -validation::email() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare email_re - email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" - [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 -} - -# @description Validate whether a given input is a valid IP V4 address. -# -# @example -# ips=' -# 4.2.2.2 -# a.b.c.d -# 192.168.1.1 -# 0.0.0.0 -# 255.255.255.255 -# 255.255.255.256 -# 192.168.0.1 -# 192.168.0 -# 1234.123.123.123 -# 0.192.168.1 -# ' -# for ip in $ips; do -# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi -# printf "%-20s: %s\n" "$ip" "$stat" -# done -# #Output -# 4.2.2.2 : good -# a.b.c.d : bad -# 192.168.1.1 : good -# 0.0.0.0 : good -# 255.255.255.255 : good -# 255.255.255.256 : bad -# 192.168.0.1 : good -# 192.168.0 : bad -# 1234.123.123.123 : bad -# 0.192.168.1 : good -# -# @arg $1 string input IPv4 address. -# -# @exitcode 0 If provided input is a valid IPv4. -# @exitcode 1 If provided input is not a valid IPv4. -# @exitcode 2 Function missing arguments. -validation::ipv4() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare ip="${1}" - declare IFS=. - # shellcheck disable=SC2206 - declare -a a=($ip) - [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 - # Test values of quads - declare quad - for quad in {0..3}; do - [[ "${a[$quad]}" -gt 255 ]] && return 1 - done - return 0 -} - -# @description Validate whether a given input is a valid IP V6 address. -# -# @example -# ips=' -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 -# fe80::1ff:fe23:4567:890a -# fe80::1ff:fe23:4567:890a%eth2 -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar -# fezy::1ff:fe23:4567:890a -# :: -# 2001:db8:: -# ' -# for ip in $ips; do -# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi -# printf "%-50s= %s\n" "$ip" "$stat" -# done -# #Output -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -# fe80::1ff:fe23:4567:890a = good -# fe80::1ff:fe23:4567:890a%eth2 = good -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -# fezy::1ff:fe23:4567:890a = bad -# :: = good -# 2001:db8:: = good -# -# @arg $1 string input IPv6 address. -# -# @exitcode 0 If provided input is a valid IPv6. -# @exitcode 1 If provided input is not a valid IPv6. -# @exitcode 2 Function missing arguments. -validation::ipv6() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare ip="${1}" - declare 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]))$" - - [[ "${ip}" =~ $re ]] && return 0 || return 1 -} - -# @description Validate if given variable is entirely alphabetic characters. -# -# @example -# test='abcABC' -# validation::alpha "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is only alpha characters. -# @exitcode 1 If input contains any non alpha characters. -# @exitcode 2 Function missing arguments. -validation::alpha() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable contains only alpha-numeric characters. -# -# @example -# test='abc123' -# validation::alpha_num "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is an alpha-numeric. -# @exitcode 1 If input is not an alpha-numeric. -# @exitcode 2 Function missing arguments. -validation::alpha_num() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alnum:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. -# -# @example -# test='abc-ABC_cD' -# validation::alpha_dash "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is valid. -# @exitcode 1 If input the input is not valid. -# @exitcode 2 Function missing arguments. -validation::alpha_dash() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]_-]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. -# -# @arg $1 string Version number to check (eg: 1.0.1) -# $arg $2 string Version number to check (eg: 1.0.1) -# -# @example -# test='abc-ABC_cD' -# validation::version_comparison "12.0.1" "12.0.1" -# echo $? -# #Output -# 0 -# -# @exitcode 0 version number is equal. -# @exitcode 1 $1 version number is greater than $2. -# @exitcode 2 $1 version number is less than $2. -# @exitcode 3 Function is missing required arguments. -# @exitcode 4 Provided input argument is in invalid format. -validation::version_comparison() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 - - declare regex="^[.0-9]*$" - ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 - ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 - - if [[ "$1" == "$2" ]]; then - return 0 - fi - declare IFS=. - declare -a ver1 ver2 - read -r -a ver1 <<<"${1}" - read -r -a ver2 <<<"${2}" - # fill empty fields in ver1 with zeros - for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do - ver1[i]=0 - done - for ((i = 0; i < ${#ver1[@]}; i++)); do - if [[ -z ${ver2[i]} ]]; then - # fill empty fields in ver2 with zeros - ver2[i]=0 - fi - if ((10#${ver1[i]} > 10#${ver2[i]})); then - return 1 - fi - if ((10#${ver1[i]} < 10#${ver2[i]})); then - return 2 - fi - done - return 0 -} diff --git a/src/bash-utility/src/variable.sh b/src/bash-utility/src/variable.sh deleted file mode 100644 index 67e9fab55..000000000 --- a/src/bash-utility/src/variable.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash - -# @file Variable -# @brief Functions for handling variables. - -# @description Check if given variable is array. -# Pass the variable name instead of value of the variable. -# -# @example -# arr=("a" "b" "c") -# variable::is_array "arr" -# #Output -# 0 -# -# @arg $1 string name of the variable to check. -# -# @exitcode 0 If input is array. -# @exitcode 1 If input is not an array. -variable::is_array() { - if [[ -z "${1}" ]]; then - return 1 - else - declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 - fi - return 1 -} - -# @description Check if given variable is a number. -# -# @example -# variable::is_numeric "1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is number. -# @exitcode 1 If input is not a number. -variable::is_numeric() { - declare re='^[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is an integer. -# -# @example -# variable::is_int "+1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is an integer. -# @exitcode 1 If input is not an integer. -variable::is_int() { - declare re='^[+-]?[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a float. -# -# @example -# variable::is_float "+1234.0" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a float. -# @exitcode 1 If input is not a float. -variable::is_float() { - declare re='^[+-]?[0-9]+.?[0-9]*$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a boolean. -# -# @example -# variable::is_bool "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a boolean. -# @exitcode 1 If input is not a boolean. -variable::is_bool() { - [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 -} - -# @description Check if given variable is a true. -# -# @example -# variable::is_true "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is true. -# @exitcode 1 If input is not true. -variable::is_true() { - [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 -} - -# @description Check if given variable is false. -# -# @example -# variable::is_false "false" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is false. -# @exitcode 1 If input is not false. -variable::is_false() { - [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 -} - -# @description Check if given variable is empty or null. -# -# @example -# test='' -# variable::is_empty_or_null $test -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is empty or null. -# @exitcode 1 If input is not empty or null. -variable::is_empty_or_null() { - [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 -} diff --git a/src/configng/README.md b/src/configng/README.md deleted file mode 100644 index 1653df4c9..000000000 --- a/src/configng/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# configng -This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility) -embedded in this project. This allows for functional programming in Bash and also modernizes -the monolithic nature of armbian-config. Error handling and validation are also included. -The idea is to provide an API in Bash that can be called from a TUI, GUI or CLI. Please -follow the coding standards which follow Bash Utility functions. - -Why Bash? Well, because it's going to be in every distribution. Striped down distributions -may not include Python, C/C++, etc. build/runtime environments - -## Quick start -* `sudo apt install git` -* `cd ~/` -* `git clone https://github.com/armbian/configng.git` -* `cd ~/configng/test` -* `sudo ./cpu_test.sh` -If all goes well you should see all the functions in cpu.sh called and output diaplayed. - -## Coding standards -[Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, -but fundementally look at the code in Bash Utility: -``` -# @description Strip characters from the beginning of a string. -# -# @example -# echo "$(string::lstrip "Hello World!" "He")" -# #Output -# llo World! -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::lstrip() { -[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 -printf '%s\n' "${1##$2}" -} -``` - -Functions should follow filename::func_name style. Then you can tell just from the name which -file the function is located in. Return codes should also follow a similar pattern: -* 0 Successful -* 1 Not found -* 2 Function missing arguments -* 3-255 all other errors - -Validate values: -``` -# Validate minimum frequency is <= maximum frequency -[ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 -``` - -Return values should use stdout: -``` -# Return value -printf '%s\n' "$(cat $file)" -``` - -Only use sudo when needed and never run as root! diff --git a/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md b/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md deleted file mode 100644 index 461c926b9..000000000 --- a/src/configng/functions/bash-utility-master/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at - -For answers to common questions about this code of conduct, see - - -[homepage]: https://www.contributor-covenant.org diff --git a/src/configng/functions/bash-utility-master/CONTRIBUTING.md b/src/configng/functions/bash-utility-master/CONTRIBUTING.md deleted file mode 100644 index aa401df88..000000000 --- a/src/configng/functions/bash-utility-master/CONTRIBUTING.md +++ /dev/null @@ -1,129 +0,0 @@ -# Contributing to Bash-Utility - -:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: -The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. - -## Table of Contents -- [Code Contributions](#code-contributions) -- [Code Guidelines](#code-guidelines) - - [Styleguide](#styleguide) - - [Bashdoc guideline](#bashdoc-guideline) -- [Documentation](#documentation) -- [Commit Guidelines](#commit-guidelines) -- [Pull Request Guidelines](#pull-request-guidelines) -- [Contact](#contact) - -## Code Contributions - -Great, the more, the merrier. - -Sane code contributions are always welcome, whether to the code or documentation. - -Before making a pull request, make sure to follow below guidelines: - -### Code Guidelines - -#### Styleguide - -- Variable names must be meaningful and self-documenting. -- Long variable names must be structured by underscores to improve legibility. -- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE) -- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data) -- Variable names can be alphanumeric with underscores. No special characters in variable names. -- Variables name must not start with number. -- Variables within function must be declared. So the scope of variable is restricted to the function. -- Avoid accessing global variables within functions. -- Function names must be all lower case with underscores to seperate words (snake_case). -- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`) -- Try using bash builtins and string substitution as much as possible. -- Use printf everywhere instead of echo. -- Before adding a new logic, be sure to check the existing code. -- Make sure to add the function in appropriate section based on its operation. -- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command: - - ```shell - shfmt upload.sh - ``` - - The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags. - You can also install shfmt for various editors, refer their repo for information. - Note: This is strictly necessary to maintain consistency, do not skip. - -- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason. - -#### Bashdoc guideline - -The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is -properly generated by the generator. - -Follow the below bashdoc template to add function introductory comment. - -```bash -# @description Multiline description goes here and -# there -# -# @example -# sample::function a b c -# echo 123 -# -# @arg $1 string Some arg. -# @arg $2 any Rest of arguments. -# -# @noargs -# -# @exitcode 0 If successfull. -# @exitcode >0 On failure -# @exitcode 5 On some error. -# -# @stdout Path to something. -# -# @see sample::other_function(() -sample::function() { -} -``` - -- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used. -- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment. -- Make sure to document the exitcode emitted by the function. -- If the function is similar to other function add a reference to function using @see tag. - -### Documentation - -- Refrain from making unnecessary newlines or whitespace. -- Use pure markdown as much as possible, html is accepted but shouldn't be a priority. -- The markdown must pass RemarkLint checks. -- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc. - - ```bash - ./bin/generate_readme.sh -f README.md -s src/ - ``` - -### Commit Guidelines - -It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted. - -It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message. - -The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation. - -Before committing check for unnecessary whitespace with `git diff --check`. - -### Pull Request Guidelines - -The following guidelines will increase the likelihood that your pull request will get accepted: - -- Follow the commit and code guidelines. -- Keep the patches on topic and focused. -- Try to avoid unnecessary formatting and clean-up where reasonable. - -A pull request should contain the following: - -- At least one commit (all of which should follow the Commit Guidelines). -- Title that summarises the issue/feature. -- Description that briefly summarises the changes. - -After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread. - -## Contact - -For further inquiries, you can contact the developer by opening an issue on the repository. diff --git a/src/configng/functions/bash-utility-master/LICENSE b/src/configng/functions/bash-utility-master/LICENSE deleted file mode 100644 index 99dd0836b..000000000 --- a/src/configng/functions/bash-utility-master/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 labbots - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/configng/functions/bash-utility-master/README.md b/src/configng/functions/bash-utility-master/README.md deleted file mode 100644 index 9bc1f9484..000000000 --- a/src/configng/functions/bash-utility-master/README.md +++ /dev/null @@ -1,3026 +0,0 @@ -

Bash Utility

- -

-Stars -License - -

-

-Gh-pages Status -Website -

-

-Total number of Library functions -

-

- -

-Bash library which provides utility functions and helpers for functional programming in Bash. - -Detailed documentation is available at - - - -## Table of Contents - -- [Installation](#installation) - - [Method 1 - Git Submodules](#method-1---git-submodules) - - [Method 2 - Git Clone](#method-2---git-clone) - - [Method 3 - Direct Download](#method-3---direct-download) -- [Usage](#usage) -- [Array](#array) - - [array::contains()](#arraycontains) - - [array::dedupe()](#arraydedupe) - - [array::is_empty()](#arrayis_empty) - - [array::join()](#arrayjoin) - - [array::reverse()](#arrayreverse) - - [array::random_element()](#arrayrandom_element) - - [array::sort()](#arraysort) - - [array::rsort()](#arrayrsort) - - [array::bsort()](#arraybsort) - - [array::merge()](#arraymerge) -- [Check](#check) - - [check::command_exists()](#checkcommand_exists) - - [check::is_sudo()](#checkis_sudo) -- [Collection](#collection) - - [collection::each()](#collectioneach) - - [collection::every()](#collectionevery) - - [collection::filter()](#collectionfilter) - - [collection::find()](#collectionfind) - - [collection::invoke()](#collectioninvoke) - - [collection::map()](#collectionmap) - - [collection::reject()](#collectionreject) - - [collection::some()](#collectionsome) -- [Date](#date) - - [date::now()](#datenow) - - [date::epoc()](#dateepoc) - - [date::add_days_from()](#dateadd_days_from) - - [date::add_months_from()](#dateadd_months_from) - - [date::add_years_from()](#dateadd_years_from) - - [date::add_weeks_from()](#dateadd_weeks_from) - - [date::add_hours_from()](#dateadd_hours_from) - - [date::add_minutes_from()](#dateadd_minutes_from) - - [date::add_seconds_from()](#dateadd_seconds_from) - - [date::add_days()](#dateadd_days) - - [date::add_months()](#dateadd_months) - - [date::add_years()](#dateadd_years) - - [date::add_weeks()](#dateadd_weeks) - - [date::add_hours()](#dateadd_hours) - - [date::add_minutes()](#dateadd_minutes) - - [date::add_seconds()](#dateadd_seconds) - - [date::sub_days_from()](#datesub_days_from) - - [date::sub_months_from()](#datesub_months_from) - - [date::sub_years_from()](#datesub_years_from) - - [date::sub_weeks_from()](#datesub_weeks_from) - - [date::sub_hours_from()](#datesub_hours_from) - - [date::sub_minutes_from()](#datesub_minutes_from) - - [date::sub_seconds_from()](#datesub_seconds_from) - - [date::sub_days()](#datesub_days) - - [date::sub_months()](#datesub_months) - - [date::sub_years()](#datesub_years) - - [date::sub_weeks()](#datesub_weeks) - - [date::sub_hours()](#datesub_hours) - - [date::sub_minutes()](#datesub_minutes) - - [date::sub_seconds()](#datesub_seconds) - - [date::format()](#dateformat) -- [Debug](#debug) - - [debug::print_array()](#debugprint_array) - - [debug::print_ansi()](#debugprint_ansi) -- [File](#file) - - [file::make_temp_file()](#filemake_temp_file) - - [file::make_temp_dir()](#filemake_temp_dir) - - [file::name()](#filename) - - [file::basename()](#filebasename) - - [file::extension()](#fileextension) - - [file::dirname()](#filedirname) - - [file::full_path()](#filefull_path) - - [file::mime_type()](#filemime_type) - - [file::contains_text()](#filecontains_text) -- [Format](#format) - - [format::human_readable_seconds()](#formathuman_readable_seconds) - - [format::bytes_to_human()](#formatbytes_to_human) - - [format::strip_ansi()](#formatstrip_ansi) - - [format::text_center()](#formattext_center) - - [format::report()](#formatreport) - - [format::trim_text_to_term()](#formattrim_text_to_term) -- [Interaction](#interaction) - - [interaction::prompt_yes_no()](#interactionprompt_yes_no) - - [interaction::prompt_response()](#interactionprompt_response) -- [Json](#json) - - [json::get_value()](#jsonget_value) -- [Miscellaneous](#miscellaneous) - - [misc::check_internet_connection()](#misccheck_internet_connection) - - [misc::get_pid()](#miscget_pid) - - [misc::get_uid()](#miscget_uid) - - [misc::generate_uuid()](#miscgenerate_uuid) -- [Operating System](#operating-system) - - [os::detect_os()](#osdetect_os) - - [os::detect_linux_distro()](#osdetect_linux_distro) - - [os::detect_linux_version()](#osdetect_linux_version) - - [os::detect_mac_version()](#osdetect_mac_version) -- [String](#string) - - [string::trim()](#stringtrim) - - [string::split()](#stringsplit) - - [string::lstrip()](#stringlstrip) - - [string::rstrip()](#stringrstrip) - - [string::to_lower()](#stringto_lower) - - [string::to_upper()](#stringto_upper) - - [string::contains()](#stringcontains) - - [string::starts_with()](#stringstarts_with) - - [string::ends_with()](#stringends_with) - - [string::regex()](#stringregex) -- [Terminal](#terminal) - - [terminal::is_term()](#terminalis_term) - - [terminal::detect_profile()](#terminaldetect_profile) - - [terminal::clear_line()](#terminalclear_line) -- [Validation](#validation) - - [validation::email()](#validationemail) - - [validation::ipv4()](#validationipv4) - - [validation::ipv6()](#validationipv6) - - [validation::alpha()](#validationalpha) - - [validation::alpha_num()](#validationalpha_num) - - [validation::alpha_dash()](#validationalpha_dash) - - [validation::version_comparison()](#validationversion_comparison) -- [Variable](#variable) - - [variable::is_array()](#variableis_array) - - [variable::is_numeric()](#variableis_numeric) - - [variable::is_int()](#variableis_int) - - [variable::is_float()](#variableis_float) - - [variable::is_bool()](#variableis_bool) - - [variable::is_true()](#variableis_true) - - [variable::is_false()](#variableis_false) - - [variable::is_empty_or_null()](#variableis_empty_or_null) -- [Inspired By](#inspired-by) -- [License](#license) - - -## Installation -The script can be installed and sourced using following methods. - -### Method 1 - Git Submodules -If the library is used inside a git project then git submodules can be used to install the library to the project. -Following command will initialize git submodule and download the library to `./vendor/bash-utility` folder. - -```shell -git submodule init -git submodule add -b master https://github.com/labbots/bash-utility vendor/bash-utility -``` - -To Update submodules to latest code execute the following command. - -```shell -git submodule update --rebase --remote -``` -Once the submodule is added or updated, make sure to commit changes to your repository. - -```shell -git add . -git commit -m 'Added/updated bash-utility library.' -``` -**Note:** When cloning your repository, use `--recurse-submodules` flag to `git clone` command to install the git sub modules. - -### Method 2 - Git Clone -If you don't want to use git submodules, you can use `git clone` to download library and then move the files to desired location manually. - -The below command will clone the repository to `vendor/bash-utility` folder in current working directory. - -```shell -git clone https://github.com/labbots/bash-utility.git ./vendor/bash-utility -``` -### Method 3 - Direct Download -If you do not have git installed, you can download the archive of the latest version of the library. Extract the zip file to appropriate folder by following the below command. - -```shell -wget https://github.com/labbots/bash-utility/archive/master.zip -unzip -q master.zip -d tmp -mkdir -p vendor/bash-utility -mv tmp/bash-utility-master vendor/bash-utility -rm tmp -``` - -## Usage -Bash utility functions can be used by simply sourcing the library script file to your own script. -To access all the functions within the bash-utility library, you could import the main bash file as follows. - -```shell -source "vendor/bash-utility/bash-utility.sh" -``` - -You can also only use the necessary library functions by only importing the required function files. - -```shell -source "vendor/bash-utility/src/array.sh" -``` - - - -## Array - -Functions for array operations and manipulations. - -### array::contains() - -Check if item exists in the given array. - -#### Arguments - -- **$1** (mixed): Item to search (needle). -- **$2** (array): array to be searched (haystack). - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found in the array. -- **2**: Function missing arguments. - -#### Example - -```bash -array=("a" "b" "c") -array::contains "c" ${array[@]} -#Output -0 -``` - -### array::dedupe() - -Remove duplicate items from the array. - -#### Arguments - -- **$1** (array): Array to be deduped. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Deduplicated array. - -#### Example - -```bash -array=("a" "b" "a" "c") -printf "%s" "$(array::dedupe ${array[@]})" -#Output -a -b -c -``` - -### array::is_empty() - -Check if a given array is empty. - -#### Arguments - -- **$1** (array): Array to be checked. - -#### Exit codes - -- **0**: If the given array is empty. -- **2**: If the given array is not empty. - -#### Example - -```bash -array=("a" "b" "c" "d") -array::is_empty "${array[@]}" -``` - -### array::join() - -Join array elements with a string. - -#### Arguments - -- **$1** (string): String to join the array elements (glue). -- **$2** (array): array to be joined with glue string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- String containing a string representation of all the array elements in the same order,with the glue string between each element. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s" "$(array::join "," "${array[@]}")" -#Output -a,b,c,d -printf "%s" "$(array::join "" "${array[@]}")" -#Output -abcd -``` - -### array::reverse() - -Return an array with elements in reverse order. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The reversed array. - -#### Example - -```bash -array=(1 2 3 4 5) -printf "%s" "$(array::reverse "${array[@]}")" -#Output -5 4 3 2 1 -``` - -### array::random_element() - -Returns a random item from the array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Random item out of the array. - -#### Example - -```bash -array=("a" "b" "c" "d") -printf "%s\n" "$(array::random_element "${array[@]}")" -#Output -c -``` - -### array::sort() - -Sort an array from lowest to highest. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -1 -2 -4 5 -a -a c -d -``` - -### array::rsort() - -Sort an array in reverse order (highest to lowest). - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- reverse sorted array. - -#### Example - -```bash -sarr=("a c" "a" "d" 2 1 "4 5") -array::array_sort "${sarr[@]}" -#Output -d -a c -a -4 5 -2 -1 -``` - -### array::bsort() - -Bubble sort an integer array from lowest to highest. -This sort does not work on string array. - -#### Arguments - -- **$1** (array): The input array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- bubble sorted array. - -#### Example - -```bash -iarr=(4 5 1 3) -array::bsort "${iarr[@]}" -#Output -1 -3 -4 -5 -``` - -### array::merge() - -Merge two arrays. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of first array. -- **$2** (string): variable name of second array. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Merged array. - -#### Example - -```bash -a=("a" "c") -b=("d" "c") -array::merge "a[@]" "b[@]" -#Output -a -c -d -c -``` - -## Check - -Helper functions. - -### check::command_exists() - -Check if the command exists in the system. - -#### Arguments - -- **$1** (string): Command name to be searched. - -#### Exit codes - -- **0**: If the command exists. -- **1**: If the command does not exist. -- **2**: Function missing arguments. - -#### Example - -```bash -check::command_exists "tput" -``` - -### check::is_sudo() - -Check if the script is executed with sudo privilege. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If the script is executed with root privilege. -- **1**: If the script is not executed with root privilege - -#### Example - -```bash -check::is_sudo -``` - -## Collection - -(Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -### collection::each() - -Iterates over elements of collection and invokes iteratee for each element. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output of iteratee function. - -#### Example - -```bash -test_func(){ - printf "print value: %s\n" "$1" - return 0 - } -arr1=("a b" "c d" "a" "d") -printf "%s\n" "${arr1[@]}" | collection::each "test_func" -collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -#output - print value: a b - print value: c d - print value: a - print value: d -``` - -#### Example - -```bash -# If other function from this library is already used to process the array. -# Then following method could be used to pass the array to the function. -out=("$(array::dedupe "${arr1[@]}")") -collection::each "test_func" <<< "${out[@]}" -``` - -### collection::every() - -Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If iteratee function fails. -- **2**: Function missing arguments. - -#### Example - -```bash -arri=("1" "2" "3" "4") -printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -``` - -### collection::filter() - -Iterates over elements of array, returning all elements where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -#output -1 -2 -3 -``` - -### collection::find() - -Iterates over elements of collection, returning the first element where iteratee returns true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Output on stdout - -- first array value matching the iteratee function. - -#### Example - -```bash -arr=("1" "2" "3" "a") -check_a(){ - [[ "$1" = "a" ]] -} -printf "%s\n" "${arr[@]}" | collection::find "check_a" -#Output -a -``` - -### collection::invoke() - -Invokes the iteratee with each element passed as argument to the iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output from the iteratee function. - -#### Example - -```bash -opt=("-a" "-l") -printf "%s\n" "${opt[@]}" | collection::invoke "ls" -``` - -### collection::map() - -Creates an array of values by running each element in array through iteratee. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. -- other exitcode returned by iteratee. - -#### Output on stdout - -- Output result of iteratee on value. - -#### Example - -```bash -arri=("1" "2" "3") -add_one(){ - i=${1} - i=$(( i + 1 )) - printf "%s\n" "$i" -} -printf "%s\n" "${arri[@]}" | collection::map "add_one" -``` - -### collection::reject() - -The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- array values not matching the iteratee function. - -#### Example - -```bash -arri=("1" "2" "3" "a") -printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -#Ouput -a -``` - -#### See also - -- [collection::filter](#collectionfilter) - -### collection::some() - -Checks if iteratee returns true for any element of the array. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): Iteratee function. - -#### Exit codes - -- **0**: If match successful. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -arr=("a" "b" "3" "a") -printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -``` - -## Date - -Functions for manipulating dates. - -### date::now() - -Get current time in unix timestamp. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- current timestamp. - -#### Example - -```bash -echo "$(date::now)" -#Output -1591554426 -``` - -### date::epoc() - -convert datetime string to unix timestamp. - -#### Arguments - -- **$1** (string): date time in any format. - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp for specified datetime. - -#### Example - -```bash -echo "$(date::epoc "2020-07-07 18:38")" -#Output -1594143480 -``` - -### date::add_days_from() - -Add number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days_from "1594143480")" -#Output -1594229880 -``` - -### date::add_months_from() - -Add number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months_from "1594143480")" -#Output -1596821880 -``` - -### date::add_years_from() - -Add number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years_from "1594143480")" -#Output -1625679480 -``` - -### date::add_weeks_from() - -Add number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks_from "1594143480")" -#Output -1594748280 -``` - -### date::add_hours_from() - -Add number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours_from "1594143480")" -#Output -1594147080 -``` - -### date::add_minutes_from() - -Add number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes_from "1594143480")" -#Output -1594143540 -``` - -### date::add_seconds_from() - -Add number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds_from "1594143480")" -#Output -1594143481 -``` - -### date::add_days() - -Add number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_days "1")" -#Output -1591640826 -``` - -### date::add_months() - -Add number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_months "1")" -#Output -1594146426 -``` - -### date::add_years() - -Add number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_years "1")" -#Output -1623090426 -``` - -### date::add_weeks() - -Add number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_weeks "1")" -#Output -1592159226 -``` - -### date::add_hours() - -Add number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_hours "1")" -#Output -1591558026 -``` - -### date::add_minutes() - -Add number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_minutes "1")" -#Output -1591554486 -``` - -### date::add_seconds() - -Add number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::add_seconds "1")" -#Output -1591554427 -``` - -### date::sub_days_from() - -Subtract number of days from specified timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days_from "1594143480")" -#Output -1594057080 -``` - -### date::sub_months_from() - -Subtract number of months from specified timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months_from "1594143480")" -#Output -1591551480 -``` - -### date::sub_years_from() - -Subtract number of years from specified timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years_from "1594143480")" -#Output -1562521080 -``` - -### date::sub_weeks_from() - -Subtract number of weeks from specified timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks_from "1594143480")" -#Output -1593538680 -``` - -### date::sub_hours_from() - -Subtract number of hours from specified timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours_from "1594143480")" -#Output -1594139880 -``` - -### date::sub_minutes_from() - -Subtract number of minutes from specified timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes_from "1594143480")" -#Output -1594143420 -``` - -### date::sub_seconds_from() - -Subtract number of seconds from specified timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. -- **2**: Function missing arguments. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds_from "1594143480")" -#Output -1594143479 -``` - -### date::sub_days() - -Subtract number of days from current day timestamp. -If number of days not specified then it defaults to 1 day. - -#### Arguments - -- **$1** (int): number of days (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_days "1")" -#Output -1588876026 -``` - -### date::sub_months() - -Subtract number of months from current day timestamp. -If number of months not specified then it defaults to 1 month. - -#### Arguments - -- **$1** (int): number of months (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_months "1")" -#Output -1559932026 -``` - -### date::sub_years() - -Subtract number of years from current day timestamp. -If number of years not specified then it defaults to 1 year. - -#### Arguments - -- **$1** (int): number of years (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_years "1")" -#Output -1591468026 -``` - -### date::sub_weeks() - -Subtract number of weeks from current day timestamp. -If number of weeks not specified then it defaults to 1 week. - -#### Arguments - -- **$1** (int): number of weeks (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_weeks "1")" -#Output -1590949626 -``` - -### date::sub_hours() - -Subtract number of hours from current day timestamp. -If number of hours not specified then it defaults to 1 hour. - -#### Arguments - -- **$1** (int): number of hours (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_hours "1")" -#Output -1591550826 -``` - -### date::sub_minutes() - -Subtract number of minutes from current day timestamp. -If number of minutes not specified then it defaults to 1 minute. - -#### Arguments - -- **$1** (int): number of minutes (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_minutes "1")" -#Output -1591554366 -``` - -### date::sub_seconds() - -Subtract number of seconds from current day timestamp. -If number of seconds not specified then it defaults to 1 second. - -#### Arguments - -- **$1** (int): number of seconds (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate timestamp. - -#### Output on stdout - -- timestamp. - -#### Example - -```bash -echo "$(date::sub_seconds "1")" -#Output -1591554425 -``` - -### date::format() - -Format unix timestamp to human readable format. -If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. - -#### Arguments - -- **$1** (int): unix timestamp. -- **$2** (string): format control characters based on `date` command (optional). - -#### Exit codes - -- **0**: If successful. -- **1**: If unable to generate time string. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo echo "$(date::format "1594143480")" -#Output -2020-07-07 18:38:00 -``` - -## Debug - -Functions to facilitate debugging scripts. - -### debug::print_array() - -Prints the content of array as key value pair for easier debugging. -Pass the variable name of the array instead of value of the variable. - -#### Arguments - -- **$1** (string): variable name of the array. - -#### Output on stdout - -- Formatted key value of array. - -#### Example - -```bash -array=(foo bar baz) -printf "Array\n" -printarr "array" -declare -A assoc_array -assoc_array=([foo]=bar [baz]=foobar) -printf "Assoc Array\n" -printarr "assoc_array" -#Output -Array -0 = foo -1 = bar -2 = baz -Assoc Array -baz = foobar -foo = bar -``` - -### debug::print_ansi() - -Function to print ansi escape sequence as is. -This function helps debug ansi escape sequence in text by displaying the escape codes. - -#### Arguments - -- **$1** (string): input with ansi escape sequence. - -#### Output on stdout - -- Ansi escape sequence printed in output as is. - -#### Example - -```bash -txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -debug::print_ansi "$txt" -#Output -\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -``` - -## File - -Functions for handling files. - -### file::make_temp_file() - -Create temporary file. -Function creates temporary file with random name. The temporary file will be deleted when script finishes. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp file. - -#### Output on stdout - -- file name of temporary file created. - -#### Example - -```bash -echo "$(file::make_temp_file)" -#Output -tmp.vgftzy -``` - -### file::make_temp_dir() - -Create temporary directory. -Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. - -#### Arguments - -- **$1** (string): Temporary directory prefix -- $2 string Flag to auto remove directory on exit trap (true) - -#### Exit codes - -- **0**: If successful. -- **1**: If failed to create temp directory. -- **2**: Missing arguments. - -#### Output on stdout - -- directory name of temporary directory created. - -#### Example - -```bash -echo "$(utility::make_temp_dir)" -#Output -tmp.rtfsxy -``` - -### file::name() - -Get only the filename from string path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- name of the file with extension. - -#### Example - -```bash -echo "$(file::name "/path/to/test.md")" -#Output -test.md -``` - -### file::basename() - -Get the basename of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- basename of the file. - -#### Example - -```bash -echo "$(file::basename "/path/to/test.md")" -#Output -test -``` - -### file::extension() - -Get the extension of file from file name. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **1**: If no extension is found in the filename. -- **2**: Function missing arguments. - -#### Output on stdout - -- extension of the file. - -#### Example - -```bash -echo "$(file::extension "/path/to/test.md")" -#Output -md -``` - -### file::dirname() - -Get directory name from file path. - -#### Arguments - -- **$1** (string): path. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- directory path. - -#### Example - -```bash -echo "$(file::dirname "/path/to/test.md")" -#Output -/path/to -``` - -### file::full_path() - -Get absolute path of file or directory. - -#### Arguments - -- **$1** (string): relative or absolute path to file/direcotry. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. - -#### Output on stdout - -- Absolute path to file/directory. - -#### Example - -```bash -file::full_path "../path/to/file.md" -#Output -/home/labbots/docs/path/to/file.md -``` - -### file::mime_type() - -Get mime type of provided input. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. - -#### Exit codes - -- **0**: If successful. -- **1**: If file/directory does not exist. -- **2**: Function missing arguments. -- **3**: If file or mimetype command not found in system. - -#### Output on stdout - -- mime type of file/directory. - -#### Example - -```bash -file::mime_type "../src/file.sh" -#Output -application/x-shellscript -``` - -### file::contains_text() - -Search if a given pattern is found in file. - -#### Arguments - -- **$1** (string): relative or absolute path to file/directory. -- **$2** (string): search key or regular expression. - -#### Exit codes - -- **0**: If given search parameter is found in file. -- **1**: If search paramter not found in file. -- **2**: Function missing arguments. - -#### Example - -```bash -file::contains_text "./file.sh" "^[ @[:alpha:]]*" -file::contains_text "./file.sh" "@file" -#Output -0 -``` - -## Format - -Functions to format provided input. - -### format::human_readable_seconds() - -Format seconds to human readable format. - -#### Arguments - -- **$1** (int): number of seconds. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted time string. - -#### Example - -```bash -echo "$(format::human_readable_seconds "356786")" -#Output -4 days 3 hours 6 minute(s) and 26 seconds -``` - -### format::bytes_to_human() - -Format bytes to human readable format. - -#### Arguments - -- **$1** (int): size in bytes. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted file size string. - -#### Example - -```bash -echo "$(format::bytes_to_human "2250")" -#Output -2.19 KB -``` - -### format::strip_ansi() - -Remove Ansi escape sequences from given text. - -#### Arguments - -- **$1** (string): Input text to be ansi stripped. - -#### Exit codes - -- **0**: If successful. - -#### Output on stdout - -- Ansi stripped text. - -#### Example - -```bash -format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -#Output -This is bold red text.This is green text. -``` - -### format::text_center() - -Prints the given text to centre of terminal. - -#### Arguments - -- **$1** (string): Text to be printed. -- **$2** (string): Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::text_center "This text is in centre of the terminal." "-" -``` - -### format::report() - -Format String to print beautiful report. - -#### Arguments - -- **$1** (string): Text to be printed on the left. -- **$2** (string): Text to be printed within the square brackets. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- formatted text. - -#### Example - -```bash -format::report "Initialising mission state" "Success" -#Output -Initialising mission state ....................................................................[ Success ] -``` - -### format::trim_text_to_term() - -Trim given text to width of the terminal window. - -#### Arguments - -- **$1** (string): Text of first sentence. -- **$2** (string): Text of second sentence (optional). - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- trimmed text. - -#### Example - -```bash -format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -#Output -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -``` - -## Interaction - -Functions to enable interaction with the user. - -### interaction::prompt_yes_no() - -Prompt yes or no question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer \[yes/no\] (optional). - -#### Exit codes - -- **0**: If user responds with yes. -- **1**: If user responds with no. -- **2**: Function missing arguments. - -#### Example - -```bash -interaction::prompt_yes_no "Are you sure to proceed" "yes" -#Output -Are you sure to proceed (y/n)? [y] -``` - -### interaction::prompt_response() - -Prompt question to the user. - -#### Arguments - -- **$1** (string): The question to be prompted to the user. -- **$2** (string): default answer (optional). - -#### Exit codes - -- **0**: If user responds with answer. -- **2**: Function missing arguments. - -#### Output on stdout - -- User entered answer to the question. - -#### Example - -```bash -interaction::prompt_response "Choose directory to install" "/home/path" -#Output -Choose directory to install? [/home/path] -``` - -## Json - -Simple json manipulation. These functions does not completely replace `jq` in any way. - -### json::get_value() - -Extract value from json based on key and position. -Input to the function can be a pipe output, here-string or file. - -#### Arguments - -- **$1** (string): id of the field to fetch. -- **$2** (int): position of value to extract.Defaults to 1.(optional) - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string value of extracted key. - -#### Example - -```bash -json::get_value "id" "1" < json_file -json::get_value "id" <<< "${json_var}" -echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -``` - -## Miscellaneous - -Set of miscellaneous helper functions. - -### misc::check_internet_connection() - -Check if internet connection is available. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script can connect to internet. -- **1**: If script cannot access internet. - -#### Example - -```bash -misc::check_internet_connection -``` - -### misc::get_pid() - -Get list of process ids based on process name. - -#### Arguments - -- **$1** (Name): of the process to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- list of process ids. - -#### Example - -```bash -misc::get_pid "chrome" -#Ouput -25951 -26043 -26528 -26561 -``` - -### misc::get_uid() - -Get user id based on username. - -#### Arguments - -- **$1** (username): to search. - -#### Exit codes - -- **0**: If match successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- string uid for the username. - -#### Example - -```bash -misc::get_uid "labbots" -#Ouput -1000 -``` - -### misc::generate_uuid() - -Generate random uuid. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If match successful. - -#### Output on stdout - -- random generated uuid. - -#### Example - -```bash -misc::generate_uuid -#Ouput -65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -``` - -## Operating System - -Functions to detect Operating system and version. - -### os::detect_os() - -Identify the OS the function is run on. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If OS is successfully detected. -- **1**: If unable to detect OS. - -#### Output on stdout - -- Operating system name (linux, mac or windows). - -#### Example - -```bash -os::detect_os -#Output -linux -``` - -### os::detect_linux_distro() - -Identify the distribution flavour of linux. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux distro is successfully detected. -- **1**: If unable to detect OS distro. - -#### Output on stdout - -- Linux OS distribution name (ubuntu, debian, suse, etc.,). - -#### Example - -```bash -os::detect_linux_distro -#Output -ubuntu -``` - -### os::detect_linux_version() - -Identify the Linux version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If Linux version is successfully detected. -- **1**: If unable to detect Linux version. - -#### Output on stdout - -- Linux OS version number (18.04, 20.04, etc.,). - -#### Example - -```bash -os::detect_linux_version -#Output -20.04 -``` - -### os::detect_mac_version() - -Identify the MacOS version. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If MacOS version is successfully detected. -- **1**: If unable to detect MacOS version. - -#### Output on stdout - -- MacOS version number (10.15.6, etc.,) - -#### Example - -```bash -os::detect_linux_version -#Output -10.15.7 -``` - -## String - -Functions for string operations and manipulations. - -### string::trim() - -Strip whitespace from the beginning and end of a string. - -#### Arguments - -- **$1** (string): The string to be trimmed. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- The trimmed string. - -#### Example - -```bash -echo "$(string::trim " Hello World! ")" -#Output -Hello World! -``` - -### string::split() - -Split a string to array by a delimiter. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The delimiter string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns an array of strings created by splitting the string parameter by the delimiter. - -#### Example - -```bash -array=( $(string::split "a,b,c" ",") ) -printf "%s" "$(string::split "Hello!World" "!")" -#Output -Hello -World -``` - -### string::lstrip() - -Strip characters from the beginning of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::lstrip "Hello World!" "He")" -#Output -llo World! -``` - -### string::rstrip() - -Strip characters from the end of a string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The characters you want to strip. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the modified string. - -#### Example - -```bash -echo "$(string::rstrip "Hello World!" "d!")" -#Output -Hello Worl -``` - -### string::to_lower() - -Make a string lowercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the lowercased string. - -#### Example - -```bash -echo "$(string::to_lower "HellO")" -#Output -hello -``` - -### string::to_upper() - -Make a string all uppercase. - -#### Arguments - -- **$1** (string): The input string. - -#### Exit codes - -- **0**: If successful. -- **2**: Function missing arguments. - -#### Output on stdout - -- Returns the uppercased string. - -#### Example - -```bash -echo "$(string::to_upper "HellO")" -#Output -HELLO -``` - -### string::contains() - -Check whether the search string exists within the input string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::contains "Hello World!" "lo" -``` - -### string::starts_with() - -Check whether the input string starts with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::starts_with "Hello World!" "He" -``` - -### string::ends_with() - -Check whether the input string ends with key string. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::ends_with "Hello World!" "d!" -``` - -### string::regex() - -Check whether the input string matches the given regex. - -#### Arguments - -- **$1** (string): The input string. -- **$2** (string): The search key. - -#### Exit codes - -- **0**: If match found. -- **1**: If no match found. -- **2**: Function missing arguments. - -#### Example - -```bash -string::regex "HELLO" "^[A-Z]*$" -``` - -## Terminal - -Set of useful terminal functions. - -### terminal::is_term() - -Check if script is run in terminal. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -### terminal::detect_profile() - -Detect profile rc file for zsh and bash of current script running user. - -*Function has no arguments.* - -#### Exit codes - -- **0**: If script is run on terminal. -- **1**: If script is not run on terminal. - -#### Output on stdout - -- path to the profile file. - -### terminal::clear_line() - -Clear the output in terminal on the specified line number. -This function clears line only on terminal. - -#### Arguments - -- **$1** (Line): number to clear. Defaults to 1. (optional) - -#### Exit codes - -- **0**: If script is run on terminal. - -#### Output on stdout - -- clear line ansi code. - -## Validation - -Functions to perform validation on given data. - -### validation::email() - -Validate whether a given input is a valid email address or not. - -#### Arguments - -- **$1** (string): input email address to validate. - -#### Exit codes - -- **0**: If provided input is an email address. -- **1**: If provided input is not an email address. -- **2**: Function missing arguments. - -#### Example - -```bash -test='test@gmail.com' -validation::email "${test}" -echo $? -#Output -0 -``` - -### validation::ipv4() - -Validate whether a given input is a valid IP V4 address. - -#### Arguments - -- **$1** (string): input IPv4 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv4. -- **1**: If provided input is not a valid IPv4. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 4.2.2.2 - a.b.c.d - 192.168.1.1 - 0.0.0.0 - 255.255.255.255 - 255.255.255.256 - 192.168.0.1 - 192.168.0 - 1234.123.123.123 - 0.192.168.1 - ' -for ip in $ips; do - if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi - printf "%-20s: %s\n" "$ip" "$stat" -done -#Output -4.2.2.2 : good -a.b.c.d : bad -192.168.1.1 : good -0.0.0.0 : good -255.255.255.255 : good -255.255.255.256 : bad -192.168.0.1 : good -192.168.0 : bad -1234.123.123.123 : bad -0.192.168.1 : good -``` - -### validation::ipv6() - -Validate whether a given input is a valid IP V6 address. - -#### Arguments - -- **$1** (string): input IPv6 address. - -#### Exit codes - -- **0**: If provided input is a valid IPv6. -- **1**: If provided input is not a valid IPv6. -- **2**: Function missing arguments. - -#### Example - -```bash -ips=' - 2001:db8:85a3:8d3:1319:8a2e:370:7348 - fe80::1ff:fe23:4567:890a - fe80::1ff:fe23:4567:890a%eth2 - 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar - fezy::1ff:fe23:4567:890a - :: - 2001:db8:: - ' -for ip in $ips; do - if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi - printf "%-50s= %s\n" "$ip" "$stat" -done -#Output -2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -fe80::1ff:fe23:4567:890a = good -fe80::1ff:fe23:4567:890a%eth2 = good -2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -fezy::1ff:fe23:4567:890a = bad -:: = good -2001:db8:: = good -``` - -### validation::alpha() - -Validate if given variable is entirely alphabetic characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is only alpha characters. -- **1**: If input contains any non alpha characters. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abcABC' -validation::alpha "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_num() - -Check if given variable contains only alpha-numeric characters. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is an alpha-numeric. -- **1**: If input is not an alpha-numeric. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc123' -validation::alpha_num "${test}" -echo $? -#Output -0 -``` - -### validation::alpha_dash() - -Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. - -#### Arguments - -- **$1** (string): Value of variable to validate. - -#### Exit codes - -- **0**: If input is valid. -- **1**: If input the input is not valid. -- **2**: Function missing arguments. - -#### Example - -```bash -test='abc-ABC_cD' -validation::alpha_dash "${test}" -echo $? -#Output -0 -``` - -### validation::version_comparison() - -Compares version numbers and provides return based on whether the value in equal, less than or greater. - -#### Arguments - -- **$1** (string): Version number to check (eg: 1.0.1) - -#### Exit codes - -- **0**: version number is equal. -- **1**: $1 version number is greater than $2. -- **2**: $1 version number is less than $2. -- **3**: Function is missing required arguments. -- **4**: Provided input argument is in invalid format. - -#### Example - -```bash -test='abc-ABC_cD' -validation::version_comparison "12.0.1" "12.0.1" -echo $? -#Output -0 -``` - -## Variable - -Functions for handling variables. - -### variable::is_array() - -Check if given variable is array. -Pass the variable name instead of value of the variable. - -#### Arguments - -- **$1** (string): name of the variable to check. - -#### Exit codes - -- **0**: If input is array. -- **1**: If input is not an array. - -#### Example - -```bash -arr=("a" "b" "c") -variable::is_array "arr" -#Output -0 -``` - -### variable::is_numeric() - -Check if given variable is a number. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is number. -- **1**: If input is not a number. - -#### Example - -```bash -variable::is_numeric "1234" -#Output -0 -``` - -### variable::is_int() - -Check if given variable is an integer. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is an integer. -- **1**: If input is not an integer. - -#### Example - -```bash -variable::is_int "+1234" -#Output -0 -``` - -### variable::is_float() - -Check if given variable is a float. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a float. -- **1**: If input is not a float. - -#### Example - -```bash -variable::is_float "+1234.0" -#Output -0 -``` - -### variable::is_bool() - -Check if given variable is a boolean. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is a boolean. -- **1**: If input is not a boolean. - -#### Example - -```bash -variable::is_bool "true" -#Output -0 -``` - -### variable::is_true() - -Check if given variable is a true. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is true. -- **1**: If input is not true. - -#### Example - -```bash -variable::is_true "true" -#Output -0 -``` - -### variable::is_false() - -Check if given variable is false. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is false. -- **1**: If input is not false. - -#### Example - -```bash -variable::is_false "false" -#Output -0 -``` - -### variable::is_empty_or_null() - -Check if given variable is empty or null. - -#### Arguments - -- **$1** (mixed): Value of variable to check. - -#### Exit codes - -- **0**: If input is empty or null. -- **1**: If input is not empty or null. - -#### Example - -```bash -test='' -variable::is_empty_or_null $test -#Output -0 -``` - - - -## Inspired By - -- [Bash Bible](https://github.com/dylanaraps/pure-bash-bible) - A collection of pure bash alternatives to external processes. - -## License - -[MIT](https://github.com/labbots/google-drive-upload/blob/master/LICENSE) diff --git a/src/configng/functions/bash-utility-master/bash_utility.sh b/src/configng/functions/bash-utility-master/bash_utility.sh deleted file mode 100644 index 65411add0..000000000 --- a/src/configng/functions/bash-utility-master/bash_utility.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck disable=SC1091 -source src/array.sh -source src/string.sh -source src/variable.sh -source src/file.sh -source src/misc.sh -source src/date.sh -source src/interaction.sh -source src/check.sh -source src/format.sh -source src/collection.sh -source src/json.sh -source src/terminal.sh -source src/validation.sh -source src/debug.sh -source src/os.sh - - diff --git a/src/configng/functions/bash-utility-master/bin/bashdoc.awk b/src/configng/functions/bash-utility-master/bin/bashdoc.awk deleted file mode 100644 index 13efdaf7b..000000000 --- a/src/configng/functions/bash-utility-master/bin/bashdoc.awk +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/awk -f - -# Varibles -# style = readme or doc -# toc = true or false -BEGIN { - if (! style) { - style = "doc" - } - - if (! toc) { - toc = 0 - } - - styles["empty", "from"] = ".*" - styles["empty", "to"] = "" - - styles["h1", "from"] = ".*" - styles["h1", "to"] = "# &" - - styles["h2", "from"] = ".*" - styles["h2", "to"] = "## &" - - styles["h3", "from"] = ".*" - styles["h3", "to"] = "### &" - - styles["h4", "from"] = ".*" - styles["h4", "to"] = "#### &" - - styles["h5", "from"] = ".*" - styles["h5", "to"] = "##### &" - - styles["code", "from"] = ".*" - styles["code", "to"] = "```&" - - styles["/code", "to"] = "```" - - styles["argN", "from"] = "^(\\$[0-9]) (\\S+)" - styles["argN", "to"] = "**\\1** (\\2):" - - styles["arg@", "from"] = "^\\$@ (\\S+)" - styles["arg@", "to"] = "**...** (\\1):" - - styles["li", "from"] = ".*" - styles["li", "to"] = "- &" - - styles["i", "from"] = ".*" - styles["i", "to"] = "*&*" - - styles["anchor", "from"] = ".*" - styles["anchor", "to"] = "[&](#&)" - - styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" - styles["exitcode", "to"] = "**\\1**: \\2" - - styles["h_rule", "to"] = "---" - - styles["comment", "from"] = ".*" - styles["comment", "to"] = "" - - - output_format["readme", "h1"] = "h2" - output_format["readme", "h2"] = "h3" - output_format["readme", "h3"] = "h4" - output_format["readme", "h4"] = "h5" - - output_format["bashdoc", "h1"] = "h1" - output_format["bashdoc", "h2"] = "h2" - output_format["bashdoc", "h3"] = "h3" - output_format["bashdoc", "h4"] = "h4" - - output_format["webdoc", "h1"] = "empty" - output_format["webdoc", "h2"] = "h3" - output_format["webdoc", "h3"] = "h4" - output_format["webdoc", "h4"] = "h5" - -} - -function render(type, text) { - if((style,type) in output_format){ - type = output_format[style,type] - } - return gensub( \ - styles[type, "from"], - styles[type, "to"], - "g", - text \ - ) -} - -function render_list(item, anchor) { - return "- [" item "](#" anchor ")" -} - -function generate_anchor(text) { - # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 - text = tolower(text) - gsub(/[^[:alnum:]_ -]/, "", text) - gsub(/ /, "-", text) - return text -} - -function reset() { - has_example = 0 - has_args = 0 - has_exitcode = 0 - has_stdout = 0 - - content_desc = "" - content_example = "" - content_args = "" - content_exitcode = "" - content_seealso = "" - content_stdout = "" -} - -/^[[:space:]]*# @internal/ { - is_internal = 1 -} - -/^[[:space:]]*# @file/ { - sub(/^[[:space:]]*# @file /, "") - - filedoc = render("h1", $0) "\n" - if(style == "webdoc"){ - filedoc = filedoc render("comment", "file=" $0) "\n" - } - -} - -/^[[:space:]]*# @brief/ { - sub(/^[[:space:]]*# @brief /, "") - if(style == "webdoc"){ - filedoc = filedoc render("comment", "brief=" $0) "\n" - } - filedoc = filedoc "\n" $0 -} - -/^[[:space:]]*# @description/ { - in_description = 1 - in_example = 0 - - reset() - - docblock = "" -} - -in_description { - if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) { - if (!match(content_desc, /\n$/)) { - content_desc = content_desc "\n" - } - in_description = 0 - } else { - sub(/^[[:space:]]*# @description /, "") - sub(/^[[:space:]]*# /, "") - sub(/^[[:space:]]*#$/, "") - - content_desc = content_desc "\n" $0 - } -} - -in_example { - - if (! /^[[:space:]]*#[ ]{3}/) { - - in_example = 0 - - content_example = content_example "\n" render("/code") "\n" - } else { - sub(/^[[:space:]]*#[ ]{3}/, "") - - content_example = content_example "\n" $0 - } -} - -/^[[:space:]]*# @example/ { - in_example = 1 - content_example = content_example "\n" render("h3", "Example") - content_example = content_example "\n\n" render("code", "bash") -} - -/^[[:space:]]*# @arg/ { - if (!has_args) { - has_args = 1 - - content_args = content_args "\n" render("h3", "Arguments") "\n\n" - } - - sub(/^[[:space:]]*# @arg /, "") - - $0 = render("argN", $0) - $0 = render("arg@", $0) - - content_args = content_args render("li", $0) "\n" -} - -/^[[:space:]]*# @noargs/ { - content_args = content_args "\n" render("i", "Function has no arguments.") "\n" -} - -/^[[:space:]]*# @exitcode/ { - if (!has_exitcode) { - has_exitcode = 1 - - content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n" - } - - sub(/^[[:space:]]*# @exitcode /, "") - - $0 = render("exitcode", $0) - - content_exitcode = content_exitcode render("li", $0) "\n" -} - -/^[[:space:]]*# @see/ { - sub(/[[:space:]]*# @see /, "") - anchor = generate_anchor($0) - $0 = render_list($0, anchor) - - content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n" -} - -/^[[:space:]]*# @stdout/ { - has_stdout = 1 - - sub(/^[[:space:]]*# @stdout /, "") - - content_stdout = content_stdout "\n" render("h3", "Output on stdout") - content_stdout = content_stdout "\n\n" render("li", $0) "\n" -} - -{ - docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso - if(style == "webdoc"){ - docblock = docblock "\n" render("h_rule") "\n" - } -} - -/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example { - if (is_internal) { - is_internal = 0 - } else { - func_name = gensub(\ - /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \ - "\\3()", \ - "g" \ - ) - doc = doc "\n" render("h2", func_name) "\n" docblock - if (toc) { - url = generate_anchor(func_name) - - content_idx = content_idx "\n" "- [" func_name "](#" url ")" - } - } - - docblock = "" - reset() -} - -END { - if (filedoc != "") { - print filedoc - } - - if (toc) { - print "" - print render("h2", "Table of Contents") - print content_idx - print "" - print render("h_rule") - } - - print doc -} diff --git a/src/configng/functions/bash-utility-master/bin/generate_readme.sh b/src/configng/functions/bash-utility-master/bin/generate_readme.sh deleted file mode 100644 index 858a4b8be..000000000 --- a/src/configng/functions/bash-utility-master/bin/generate_readme.sh +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env bash - -#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh -#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh -_usage() { - printf " -Script to autogenerate markdown based on bash source code.\n -The script generates table of contents and bashdoc and update the given markdown file.\n -Usage:\n %s [options.. ]\n -Options:\n - -f | --file - Relative or absolute path to the README.md file. - -s | --sh-dir - path to the bash script source folder to generate shdocs.\n - -l | --toc-level - Minimum level of header to print in Table of Contents.\n - -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n - -w | --webdoc - Flag to indicate generation of webdoc.\n - -p | --dest-dir - Path in which wedoc files must be generated.\n - -h | --help - Display usage instructions.\n" "${0##*/}" - exit 0 -} - -_setup_arguments() { - - unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR - MINLEVEL=1 - MAXLEVEL=3 - SCRIPT_FILE="${0##*/}" - declare source="${BASH_SOURCE[0]}" - while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - source="$(readlink "$source")" - [[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located - done - SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" - SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md" - SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src" - WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions" - - SHORTOPTS="whp:f:m:d:s:-:" - - while getopts "${SHORTOPTS}" OPTION; do - case "${OPTION}" in - -) - _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } - case "${OPTARG}" in - help) - _usage - ;; - file) - _check_longoptions "${!OPTIND}" - SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-level) - _check_longoptions "${!OPTIND}" - MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - toc-depth) - _check_longoptions "${!OPTIND}" - MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - sh-dir) - _check_longoptions "${!OPTIND}" - SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) - ;; - webdoc) - WEBDOC=true - ;; - dest-dir) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - '') - _usage - ;; - *) - printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - ;; - h) - _usage - ;; - f) - SOURCE_MARKDOWN="${OPTARG}" - ;; - m) - MINLEVEL="${OPTARG}" - ;; - d) - MAXLEVEL="${OPTARG}" - ;; - s) - SOURCE_SCRIPT_DIR="${OPTARG}" - ;; - w) - WEBDOC=true - ;; - p) - WEBDOC_DEST_DIR="${OPTARG}" - ;; - :) - printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - ?) - printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 - ;; - esac - done - shift "$((OPTIND - 1))" - - if [[ -w "${SOURCE_MARKDOWN}" ]]; then - declare src_file src_extension - src_file="${SOURCE_MARKDOWN##*/}" - src_extension="${src_file##*.}" - if [[ "${src_extension,,}" != "md" ]]; then - printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 - fi - else - printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 - fi - - if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then - printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 - fi - - declare re='^[0-9]+$' - if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then - echo "error: Not a number" >&2 - exit 1 - fi - if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then - printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 - fi - - [ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}" - -} - -_setup_tempfile() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -_generate_shdoc() { - declare file - file="$(realpath "${1}")" - if [[ -s "${file}" ]]; then - awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2" - #awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md" - fi -} - -_insert_shdoc_to_file() { - declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc - shdoc_tmp_file="$1" - source_markdown="$2" - - start_shdoc="" - info_shdoc="" - end_shdoc="" - - sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then - # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file - - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" - echo -e "Updated bashdoc content to ${source_markdown} successfully\n" - - else - { - printf "%s\n" "${start_shdoc}" - cat "${shdoc_tmp_file}" - printf "%s\n" "${end_shdoc}" - } >> "${source_markdown}" - echo -e "Created bashdoc content to ${source_markdown} successfully\n" - fi -} - -_process_sh_files() { - declare shdoc_tmp_file source_script_dir source_markdown - source_markdown="${1}" - source_script_dir="${2}" - shdoc_tmp_file=$(_setup_tempfile) - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_shdoc "${line}" "${shdoc_tmp_file}" - done - _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" - rm "${shdoc_tmp_file}" - -} - -_generate_toc() { - - declare line level title anchor output counter temp_output invalid_chars - - invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" - while IFS='' read -r line || [[ -n "${line}" ]]; do - level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" - title="$(echo "${line}" | sed -E 's/^#+ //')" - [[ "${title}" = "Table of Contents" ]] && continue - - # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text - anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" - - # check new line introduced is not duplicated, if is duplicated, introduce a number at the end - temp_output=$output"$level- [$title](#$anchor)\n" - counter=1 - while true; do - nlines="$(echo -e "${temp_output}" | wc -l)" - duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)" - if [ "${nlines}" = "${duplines}" ]; then - break - fi - temp_output=$output"$level- [$title](#$anchor-$counter)\n" - counter=$((counter + 1)) - done - - output="$temp_output" - - # grep: filter header candidates to be included in toc - # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment - done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")" - - # when in toc we have two `--` quit one - echo "$output" - -} - -_insert_toc_to_file() { - - declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash - source_markdown="${1}" - toc_text="${2}" - start_toc="" - info_toc="" - end_toc="" - - toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" - # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command - utext_ampersand="id8234923000230gzz" - utext_slash="id9992384923423gzz" - toc_block="${toc_block//\&/${utext_ampersand}}" - toc_block="${toc_block//\//${utext_slash}}" - - # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 - # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches - if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then - # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace - sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" - echo -e "Updated TOC content in ${source_markdown} succesfully\n" - - else - sed -i 1i"$toc_block" "${source_markdown}" - echo -e "Created TOC in ${source_markdown} succesfully\n" - - fi - - # undo symbol replacements - sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" - sed -i "s,${utext_slash},\/,g" "${source_markdown}" - -} - -_process_toc() { - declare toc_temp_file source_markdown level toc_text - source_markdown="${1}" - - toc_temp_file=$(_setup_tempfile) - - sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" - - level=$MINLEVEL - while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do - level=$((level + 1)) - done - - MINLEVEL=${level} - toc_text=$(_generate_toc "${toc_temp_file}") - rm "${toc_temp_file}" - - _insert_toc_to_file "${source_markdown}" "${toc_text}" -} - -_generate_webdoc() { - declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file - declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc - declare webdoc_lastmod_date webdoc_lastmod_epoc - file="$(realpath "${1}")" - dest_dir="${2}" - - filename="${file##*/}" - file_basename="${filename%.*}" - dest_file_path="${dest_dir}/${file_basename}.md" - file_modified_date="$(date -r "${file}" +"%FT%T%:z")" - file_modified_date_epoc="$(date -r "${file}" +"%s")" - - start_shdoc="" - end_shdoc="" - if [[ ! -f "${dest_file_path}" ]]; then - - cat << EOF > "${dest_file_path}" ---- -title : -description : -date : ${file_modified_date} -lastmod : ${file_modified_date} ---- -${start_shdoc} -${end_shdoc} -EOF - is_new_file=true - else - is_new_file=false - webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")" - webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")" - fi - - if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then - - shdoc_tmp_file=$(_setup_tempfile) - if [[ -s "${file}" ]]; then - awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}" - fi - - if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then - sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}" - fi - - # Extract title and description from webdoc - title="$(sed -ne 's/-->//; s/^.*//; s/^.*/${title}/g" "${dest_file_path}" - sed -i -e "s//${description}/g" "${dest_file_path}" - else - sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}" - sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}" - - fi - - # Update the last modified timestamp in front matter - sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}" - - echo -e "Updated bashdoc content to ${dest_file_path} successfully." - rm "${shdoc_tmp_file}" - - fi -} -_process_webdoc_files() { - declare source_script_dir dest_dir - - source_script_dir="${1}" - dest_dir="${2}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - while IFS= read -r -d '' line; do - _generate_webdoc "${line}" "${dest_dir}" - done -} - -_count_library_functions() { - declare source_script_dir - source_script_dir="${1}" - - find "${source_script_dir}" -name '*.sh' -print0 | sort -z | - { - declare -i function_count=0 count=0 - while IFS= read -r -d '' line; do - count=0 - count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}") - function_count=$((function_count + count)) - done - printf "Total library functions: %s \n" "${function_count}" - - } -} -main() { - # export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' - # set -x - trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT - set -o errexit -o noclobber -o pipefail - - _setup_arguments "${@}" - _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" - _process_toc "${SOURCE_MARKDOWN}" - - if [[ -n ${WEBDOC} ]]; then - _process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}" - fi - _count_library_functions "${SOURCE_SCRIPT_DIR}" -} - -main "${@}" diff --git a/src/configng/functions/bash-utility-master/image/bash-utility.png b/src/configng/functions/bash-utility-master/image/bash-utility.png deleted file mode 100644 index c1bfb8b556809ce645cf41ab6d4b11547df68fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32396 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 diff --git a/src/configng/functions/bash-utility-master/src/array.sh b/src/configng/functions/bash-utility-master/src/array.sh deleted file mode 100644 index 42d5883b8..000000000 --- a/src/configng/functions/bash-utility-master/src/array.sh +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env bash - -# @file Array -# @brief Functions for array operations and manipulations. - -# @description Check if item exists in the given array. -# -# @example -# array=("a" "b" "c") -# array::contains "c" ${array[@]} -# #Output -# 0 -# -# @arg $1 mixed Item to search (needle). -# @arg $2 array array to be searched (haystack). -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found in the array. -# @exitcode 2 Function missing arguments. -array::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare query="${1:-}" - shift - - for element in "${@}"; do - [[ "${element}" == "${query}" ]] && return 0 - done - - return 1 -} - -# @description Remove duplicate items from the array. -# -# @example -# array=("a" "b" "a" "c") -# printf "%s" "$(array::dedupe ${array[@]})" -# #Output -# a -# b -# c -# -# @arg $1 array Array to be deduped. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Deduplicated array. -array::dedupe() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -A arr_tmp - declare -a arr_unique - for i in "$@"; do - { [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue - arr_unique+=("${i}") && arr_tmp[${i}]=x - done - printf '%s\n' "${arr_unique[@]}" -} - -# @description Check if a given array is empty. -# -# @example -# array=("a" "b" "c" "d") -# array::is_empty "${array[@]}" -# -# @arg $1 array Array to be checked. -# -# @exitcode 0 If the given array is empty. -# @exitcode 2 If the given array is not empty. -array::is_empty() { - declare -a array - local array=("$@") - if [ ${#array[@]} -eq 0 ]; then - return 0 - else - return 1 - fi -} -# @description Join array elements with a string. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s" "$(array::join "," "${array[@]}")" -# #Output -# a,b,c,d -# printf "%s" "$(array::join "" "${array[@]}")" -# #Output -# abcd -# -# @arg $1 string String to join the array elements (glue). -# @arg $2 array array to be joined with glue string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element. -array::join() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare delimiter="${1}" - shift - printf "%s" "${1}" - shift - printf "%s" "${@/#/${delimiter}}" -} - -# @description Return an array with elements in reverse order. -# -# @example -# array=(1 2 3 4 5) -# printf "%s" "$(array::reverse "${array[@]}")" -# #Output -# 5 4 3 2 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The reversed array. -array::reverse() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare min=0 - declare -a array - array=("$@") - declare max=$((${#array[@]} - 1)) - - while [[ $min -lt $max ]]; do - # Swap current first and last elements - x="${array[$min]}" - array[$min]="${array[$max]}" - array[$max]="$x" - - # Move closer - ((min++, max--)) - done - printf '%s\n' "${array[@]}" -} - -# @description Returns a random item from the array. -# -# @example -# array=("a" "b" "c" "d") -# printf "%s\n" "$(array::random_element "${array[@]}")" -# #Output -# c -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Random item out of the array. -array::random_element() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array - local array=("$@") - printf '%s\n' "${array[RANDOM % $#]}" -} - -# @description Sort an array from lowest to highest. -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# 1 -# 2 -# 4 5 -# a -# a c -# d -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout sorted array. -array::sort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort <<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Sort an array in reverse order (highest to lowest). -# -# @example -# sarr=("a c" "a" "d" 2 1 "4 5") -# array::array_sort "${sarr[@]}" -# #Output -# d -# a c -# a -# 4 5 -# 2 -# 1 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout reverse sorted array. -array::rsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a array=("$@") - declare -a sorted - declare noglobtate - noglobtate="$(shopt -po noglob)" - set -o noglob - declare IFS=$'\n' - sorted=($(sort -r<<< "${array[*]}")) - unset IFS - eval "${noglobtate}" - printf "%s\n" "${sorted[@]}" -} - -# @description Bubble sort an integer array from lowest to highest. -# This sort does not work on string array. -# @example -# iarr=(4 5 1 3) -# array::bsort "${iarr[@]}" -# #Output -# 1 -# 3 -# 4 -# 5 -# -# @arg $1 array The input array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout bubble sorted array. -array::bsort() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare tmp - declare arr=("$@") - for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do - for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do - if [[ ${arr[i]} -gt ${arr[j]} ]]; then - # echo $i $j ${arr[i]} ${arr[j]} - tmp=${arr[i]} - arr[i]=${arr[j]} - arr[j]=$tmp - fi - done - done - printf "%s\n" "${arr[@]}" -} - -# @description Merge two arrays. -# Pass the variable name of the array instead of value of the variable. -# @example -# a=("a" "c") -# b=("d" "c") -# array::merge "a[@]" "b[@]" -# #Output -# a -# c -# d -# c -# -# @arg $1 string variable name of first array. -# @arg $2 string variable name of second array. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Merged array. -array::merge() { - [[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr1=("${!1}") - declare -a arr2=("${!2}") - declare out=("${arr1[@]}" "${arr2[@]}") - printf "%s\n" "${out[@]}" -} diff --git a/src/configng/functions/bash-utility-master/src/check.sh b/src/configng/functions/bash-utility-master/src/check.sh deleted file mode 100644 index 2b7c1eb1d..000000000 --- a/src/configng/functions/bash-utility-master/src/check.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# @file Check -# @brief Helper functions. - -# @description Check if the command exists in the system. -# -# @example -# check::command_exists "tput" -# -# @arg $1 string Command name to be searched. -# -# @exitcode 0 If the command exists. -# @exitcode 1 If the command does not exist. -# @exitcode 2 Function missing arguments. -check::command_exists() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - hash "${1}" 2> /dev/null -} - -# @description Check if the script is executed with sudo privilege. -# -# @example -# check::is_sudo -# -# @noargs -# -# @exitcode 0 If the script is executed with root privilege. -# @exitcode 1 If the script is not executed with root privilege -check::is_sudo() { - if [[ $(id -u) -ne 0 ]]; then - return 1 - fi -} diff --git a/src/configng/functions/bash-utility-master/src/collection.sh b/src/configng/functions/bash-utility-master/src/collection.sh deleted file mode 100644 index 9f0e244a2..000000000 --- a/src/configng/functions/bash-utility-master/src/collection.sh +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env bash - -# @file Collection -# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. - -# @description Iterates over elements of collection and invokes iteratee for each element. -# Input to the function can be a pipe output, here-string or file. -# @example -# test_func(){ -# printf "print value: %s\n" "$1" -# return 0 -# } -# arr1=("a b" "c d" "a" "d") -# printf "%s\n" "${arr1[@]}" | collection::each "test_func" -# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach -# #output -# print value: a b -# print value: c d -# print value: a -# print value: d -# -# @example -# # If other function from this library is already used to process the array. -# # Then following method could be used to pass the array to the function. -# out=("$(array::dedupe "${arr1[@]}")") -# collection::each "test_func" <<< "${out[@]}" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output of iteratee function. -collection::each() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - done -} - -# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "4") -# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If iteratee function fails. -# @exitcode 2 Function missing arguments. -collection::every() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - - if [[ $ret -ne 0 ]]; then - return 1 - fi - - done -} - -# @description Iterates over elements of array, returning all elements where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" -# #output -# 1 -# 2 -# 3 -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values matching the iteratee function. -collection::filter() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s\n" "${it}" - fi - done -} - -# @description Iterates over elements of collection, returning the first element where iteratee returns true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("1" "2" "3" "a") -# check_a(){ -# [[ "$1" = "a" ]] -# } -# printf "%s\n" "${arr[@]}" | collection::find "check_a" -# #Output -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -# -# @stdout first array value matching the iteratee function. -collection::find() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'${it}'" - fi - declare -i ret="$?" - if [[ $ret = 0 ]]; then - printf "%s" "${it}" - return 0 - fi - done - - return 1 -} - -# @description Invokes the iteratee with each element passed as argument to the iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# opt=("-a" "-l") -# printf "%s\n" "${opt[@]}" | collection::invoke "ls" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output from the iteratee function. -collection::invoke() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a args=() - declare func="${1}" - while read -r it; do - args=("${args[@]}" "$it") - done - - eval "${func}" "${args[@]}" -} - -# @description Creates an array of values by running each element in array through iteratee. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3") -# add_one(){ -# i=${1} -# i=$(( i + 1 )) -# printf "%s\n" "$i" -# } -# printf "%s\n" "${arri[@]}" | collection::map "add_one" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode other exitcode returned by iteratee. -# -# @stdout Output result of iteratee on value. -collection::map() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - declare out - - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - out="$("${func}")" - else - out="$("${func}" "$it")" - fi - - declare -i ret=$? - - if [[ $ret -ne 0 ]]; then - return $ret - fi - - printf "%s\n" "${out}" - done -} - -# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true. -# Input to the function can be a pipe output, here-string or file. -# @example -# arri=("1" "2" "3" "a") -# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" -# #Ouput -# a -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout array values not matching the iteratee function. -# @see collection::filter -collection::reject() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - echo "$it" - fi - - done -} - -# @description Checks if iteratee returns true for any element of the array. -# Input to the function can be a pipe output, here-string or file. -# @example -# arr=("a" "b" "3" "a") -# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" -# -# @arg $1 string Iteratee function. -# -# @exitcode 0 If match successful. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -collection::some() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare func="${1}" - declare IFS=$'\n' - while read -r it; do - - if [[ "${func}" == *"$"* ]]; then - eval "${func}" - else - eval "${func}" "'$it'" - fi - - declare -i ret=$? - - if [[ $ret -eq 0 ]]; then - return 0 - fi - done - - return 1 -} diff --git a/src/configng/functions/bash-utility-master/src/date.sh b/src/configng/functions/bash-utility-master/src/date.sh deleted file mode 100644 index 41f071792..000000000 --- a/src/configng/functions/bash-utility-master/src/date.sh +++ /dev/null @@ -1,744 +0,0 @@ -#!/usr/bin/env bash - -# @file Date -# @brief Functions for manipulating dates. - -# @description Get current time in unix timestamp. -# -# @example -# echo "$(date::now)" -# #Output -# 1591554426 -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout current timestamp. -date::now() { - declare now - now="$(date --universal +%s)" || return $? - printf "%s" "${now}" -} - -# @description convert datetime string to unix timestamp. -# -# @example -# echo "$(date::epoc "2020-07-07 18:38")" -# #Output -# 1594143480 -# -# @arg $1 string date time in any format. -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp for specified datetime. -date::epoc() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare date - date=$(date -d "${1}" +"%s") || return $? - printf "%s" "${date}" -} - -# @description Add number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days_from "1594143480")" -# #Output -# 1594229880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${day} day" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months_from "1594143480")" -# #Output -# 1596821880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${month} month" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years_from "1594143480")" -# #Output -# 1625679480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${year} year" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::add_weeks_from "1594143480")" -# #Output -# 1594748280 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${week} week" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours_from "1594143480")" -# #Output -# 1594147080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${hour} hour" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes_from "1594143480")" -# #Output -# 1594143540 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${minute} minute" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds_from "1594143480")" -# #Output -# 1594143481 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::add_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T')+${second} second" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::add_days "1")" -# #Output -# 1591640826 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::add_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::add_months "1")" -# #Output -# 1594146426 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::add_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_years "1")" -# #Output -# 1623090426 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::add_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 year. -# -# @example -# echo "$(date::add_weeks "1")" -# #Output -# 1592159226 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::add_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::add_hours "1")" -# #Output -# 1591558026 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::add_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::add_minutes "1")" -# #Output -# 1591554486 -# -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::add_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Add number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::add_seconds "1")" -# #Output -# 1591554427 -# -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::add_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::add_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from specified timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days_from "1594143480")" -# #Output -# 1594057080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_days_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp day - timestamp="${1}" - day=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${day} days ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from specified timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months_from "1594143480")" -# #Output -# 1591551480 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_months_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp month - timestamp="${1}" - month=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${month} months ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from specified timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years_from "1594143480")" -# #Output -# 1562521080 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_years_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp year - timestamp="${1}" - year=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${year} years ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from specified timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks_from "1594143480")" -# #Output -# 1593538680 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_weeks_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp week - timestamp="${1}" - week=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${week} weeks ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from specified timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours_from "1594143480")" -# #Output -# 1594139880 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_hours_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp hour - timestamp="${1}" - hour=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${hour} hours ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from specified timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes_from "1594143480")" -# #Output -# 1594143420 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_minutes_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - minute=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${minute} minutes ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from specified timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds_from "1594143480")" -# #Output -# 1594143479 -# -# @arg $1 int unix timestamp. -# @arg $2 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# @exitcode 2 Function missing arguments. -# -# @stdout timestamp. -date::sub_seconds_from() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp new_timestamp minute - timestamp="${1}" - second=${2:-1} - new_timestamp="$(date -d "$(date -d "@${timestamp}" '+%F %T') ${second} seconds ago" +'%s')" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of days from current day timestamp. -# If number of days not specified then it defaults to 1 day. -# -# @example -# echo "$(date::sub_days "1")" -# #Output -# 1588876026 -# -# @arg $1 int number of days (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_days() { - declare timestamp new_timestamp day - timestamp="$(date::now)" - day=${1:-1} - new_timestamp="$(date::sub_days_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of months from current day timestamp. -# If number of months not specified then it defaults to 1 month. -# -# @example -# echo "$(date::sub_months "1")" -# #Output -# 1559932026 -# -# @arg $1 int number of months (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_months() { - declare timestamp new_timestamp month - timestamp="$(date::now)" - month=${1:-1} - new_timestamp="$(date::sub_months_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of years from current day timestamp. -# If number of years not specified then it defaults to 1 year. -# -# @example -# echo "$(date::sub_years "1")" -# #Output -# 1591468026 -# -# @arg $1 int number of years (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_years() { - declare timestamp new_timestamp year - timestamp="$(date::now)" - year=${1:-1} - new_timestamp="$(date::sub_years_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of weeks from current day timestamp. -# If number of weeks not specified then it defaults to 1 week. -# -# @example -# echo "$(date::sub_weeks "1")" -# #Output -# 1590949626 -# -# @arg $1 int number of weeks (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_weeks() { - declare timestamp new_timestamp week - timestamp="$(date::now)" - week=${1:-1} - new_timestamp="$(date::sub_weeks_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of hours from current day timestamp. -# If number of hours not specified then it defaults to 1 hour. -# -# @example -# echo "$(date::sub_hours "1")" -# #Output -# 1591550826 -# -# @arg $1 int number of hours (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_hours() { - declare timestamp new_timestamp hour - timestamp="$(date::now)" - hour=${1:-1} - new_timestamp="$(date::sub_hours_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of minutes from current day timestamp. -# If number of minutes not specified then it defaults to 1 minute. -# -# @example -# echo "$(date::sub_minutes "1")" -# #Output -# 1591554366 -# -# @arg $1 int number of minutes (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_minutes() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - minute=${1:-1} - new_timestamp="$(date::sub_minutes_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Subtract number of seconds from current day timestamp. -# If number of seconds not specified then it defaults to 1 second. -# -# @example -# echo "$(date::sub_seconds "1")" -# #Output -# 1591554425 -# -# @arg $1 int number of seconds (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate timestamp. -# -# @stdout timestamp. -date::sub_seconds() { - declare timestamp new_timestamp minute - timestamp="$(date::now)" - second=${1:-1} - new_timestamp="$(date::sub_seconds_from "${timestamp}" "${second}")" || return $? - printf "%s" "${new_timestamp}" -} - -# @description Format unix timestamp to human readable format. -# If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. -# -# @example -# echo echo "$(date::format "1594143480")" -# #Output -# 2020-07-07 18:38:00 -# -# @arg $1 int unix timestamp. -# @arg $2 string format control characters based on `date` command (optional). -# -# @exitcode 0 If successful. -# @exitcode 1 If unable to generate time string. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -date::format() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare timestamp format out - timestamp="${1}" - format="${2:-"%F %T"}" - out="$(date -d "@${timestamp}" +"${format}")" || return $? - printf "%s" "${out}" - -} diff --git a/src/configng/functions/bash-utility-master/src/debug.sh b/src/configng/functions/bash-utility-master/src/debug.sh deleted file mode 100644 index 82828734f..000000000 --- a/src/configng/functions/bash-utility-master/src/debug.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash - -# @file Debug -# @brief Functions to facilitate debugging scripts. - -# @description Prints the content of array as key value pair for easier debugging. -# Pass the variable name of the array instead of value of the variable. -# @example -# array=(foo bar baz) -# printf "Array\n" -# printarr "array" -# declare -A assoc_array -# assoc_array=([foo]=bar [baz]=foobar) -# printf "Assoc Array\n" -# printarr "assoc_array" -# #Output -# Array -# 0 = foo -# 1 = bar -# 2 = baz -# Assoc Array -# baz = foobar -# foo = bar -# -# @arg $1 string variable name of the array. -# -# @stdout Formatted key value of array. -debug::print_array() { - declare -n __arr="$1" - for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done -} - -# @description Function to print ansi escape sequence as is. -# This function helps debug ansi escape sequence in text by displaying the escape codes. -# -# @example -# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" -# debug::print_ansi "$txt" -# #Output -# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m -# -# @arg $1 string input with ansi escape sequence. -# -# @stdout Ansi escape sequence printed in output as is. -debug::print_ansi() { - #echo $(tr -dc '[:print:]'<<<$1) - printf "%s\n" "${1//$'\e'/\\e}" - -} diff --git a/src/configng/functions/bash-utility-master/src/file.sh b/src/configng/functions/bash-utility-master/src/file.sh deleted file mode 100644 index 71afd8476..000000000 --- a/src/configng/functions/bash-utility-master/src/file.sh +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env bash - -# @file File -# @brief Functions for handling files. - -# @description Create temporary file. -# Function creates temporary file with random name. The temporary file will be deleted when script finishes. -# -# @example -# echo "$(file::make_temp_file)" -# #Output -# tmp.vgftzy -# -# @noargs -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp file. -# -# @stdout file name of temporary file created. -file::make_temp_file() { - declare temp_file - type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } - trap 'rm -f "${temp_file}"' EXIT - printf "%s" "${temp_file}" -} - -# @description Create temporary directory. -# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. -# -# @example -# echo "$(utility::make_temp_dir)" -# #Output -# tmp.rtfsxy -# -# @arg $1 string Temporary directory prefix -# @arg $2 string Flag to auto remove directory on exit trap (true) -# -# @exitcode 0 If successful. -# @exitcode 1 If failed to create temp directory. -# @exitcode 2 Missing arguments. -# -# @stdout directory name of temporary directory created. -file::make_temp_dir() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare temp_dir prefix="${1}" trap_rm="${2}" - temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}") - if [[ -n "${trap_rm}" ]]; then - trap 'rm -rf "${temp_dir}"' EXIT - fi - printf "%s" "${temp_dir}" -} - -# @description Get only the filename from string path. -# -# @example -# echo "$(file::name "/path/to/test.md")" -# #Output -# test.md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout name of the file with extension. -file::name() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf "%s" "${1##*/}" -} - -# @description Get the basename of file from file name. -# -# @example -# echo "$(file::basename "/path/to/test.md")" -# #Output -# test -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout basename of the file. -file::basename() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare file basename - file="${1##*/}" - basename="${file%.*}" - - printf "%s" "${basename}" -} - -# @description Get the extension of file from file name. -# -# @example -# echo "$(file::extension "/path/to/test.md")" -# #Output -# md -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 1 If no extension is found in the filename. -# @exitcode 2 Function missing arguments. -# -# @stdout extension of the file. -file::extension() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare file extension - file="${1##*/}" - extension="${file##*.}" - [[ "${file}" = "${extension}" ]] && return 1 - - printf "%s" "${extension}" -} - -# @description Get directory name from file path. -# -# @example -# echo "$(file::dirname "/path/to/test.md")" -# #Output -# /path/to -# -# @arg $1 string path. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout directory path. -file::dirname() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare tmp=${1:-.} - - [[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; } - tmp="${tmp%%"${tmp##*[!/]}"}" - - [[ ${tmp} != */* ]] && { printf '.\n' && return; } - tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}" - - printf '%s' "${tmp:-/}" -} - -# @description Get absolute path of file or directory. -# -# @example -# file::full_path "../path/to/file.md" -# #Output -# /home/labbots/docs/path/to/file.md -# -# @arg $1 string relative or absolute path to file/direcotry. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# -# @stdout Absolute path to file/directory. -file::full_path() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare input="${1}" - if [[ -f ${input} ]]; then - printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}" - elif [[ -d ${input} ]]; then - printf "%s\n" "$(cd "${input}" && pwd)" - else - return 1 - fi -} - -# @description Get mime type of provided input. -# -# @example -# file::mime_type "../src/file.sh" -# #Output -# application/x-shellscript -# -# @arg $1 string relative or absolute path to file/directory. -# -# @exitcode 0 If successful. -# @exitcode 1 If file/directory does not exist. -# @exitcode 2 Function missing arguments. -# @exitcode 3 If file or mimetype command not found in system. -# -# @stdout mime type of file/directory. -file::mime_type() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare mime_type - if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then - if type -p mimetype &> /dev/null; then - mime_type=$(mimetype --output-format %m "${1}") - elif type -p file &> /dev/null; then - mime_type=$(file --brief --mime-type "${1}") - else - return 3 - fi - else - return 1 - fi - printf "%s" "${mime_type}" -} - -# @description Search if a given pattern is found in file. -# -# @example -# file::contains_text "./file.sh" "^[ @[:alpha:]]*" -# file::contains_text "./file.sh" "@file" -# #Output -# 0 -# -# @arg $1 string relative or absolute path to file/directory. -# @arg $2 string search key or regular expression. -# -# @exitcode 0 If given search parameter is found in file. -# @exitcode 1 If search paramter not found in file. -# @exitcode 2 Function missing arguments. -file::contains_text() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - declare -r file="$1" - declare -r text="$2" - grep -q "$text" "$file" -} diff --git a/src/configng/functions/bash-utility-master/src/format.sh b/src/configng/functions/bash-utility-master/src/format.sh deleted file mode 100644 index 7dceb1d59..000000000 --- a/src/configng/functions/bash-utility-master/src/format.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash - -# @file Format -# @brief Functions to format provided input. - -# @internal -# @description Initialisation script when the code is sourced. -# -# @noargs -__init(){ -_check_terminal_window_size -} - -# @internal -# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS. -# -# @noargs -_check_terminal_window_size() { - shopt -s checkwinsize && (: && :) - trap 'shopt -s checkwinsize; (:;:)' SIGWINCH -} -# @description Format seconds to human readable format. -# -# @example -# echo "$(format::human_readable_seconds "356786")" -# #Output -# 4 days 3 hours 6 minute(s) and 26 seconds -# -# @arg $1 int number of seconds. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted time string. -format::human_readable_seconds() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare T="${1}" - declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))" - [[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}" - [[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}" - [[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}" - [[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and ' - printf '%d seconds\n' "${SEC}" -} - -# @description Format bytes to human readable format. -# -# @example -# echo "$(format::bytes_to_human "2250")" -# #Output -# 2.19 KB -# -# @arg $1 int size in bytes. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted file size string. -format::bytes_to_human() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B) - while ((b > 1024)); do - d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))" - b=$((b / 1024)) && ((s++)) - done - printf "%s\n" "${b}${d} ${S[${s}]}" -} - -# @description Remove Ansi escape sequences from given text. -# -# @example -# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" -# #Output -# This is bold red text.This is green text. -# -# @arg $1 string Input text to be ansi stripped. -# -# @exitcode 0 If successful. -# -# @stdout Ansi stripped text. -format::strip_ansi() { - declare tmp esc tpa re - tmp="${1}" - esc=$(printf "\x1b") - tpa=$(printf "\x28") - re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)" - while [[ "${tmp}" =~ $re ]]; do - tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" - done - printf "%s" "${tmp}" -} - -# @description Prints the given text to centre of terminal. -# -# @example -# format::text_center "This text is in centre of the terminal." "-" -# -# @arg $1 string Text to be printed. -# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::text_center() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare input="${1}" symbol="${2:- }" filler out no_ansi_out - no_ansi_out=$(format::strip_ansi "$input") - declare -i str_len=${#no_ansi_out} - declare -i filler_len="$(((COLUMNS - str_len) / 2))" - - [[ -n "${symbol}" ]] && symbol="${symbol:0:1}" - for ((i = 0; i < filler_len; i++)); do - filler+="${symbol}" - done - - out="${filler}${input}${filler}" - [[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}" - printf "%s" "${out}" -} - -# @description Format String to print beautiful report. -# -# @example -# format::report "Initialising mission state" "Success" -# #Output -# Initialising mission state ....................................................................[ Success ] -# -# @arg $1 string Text to be printed on the left. -# @arg $2 string Text to be printed within the square brackets. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout formatted text. -format::report() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare symbol="." to_print y hl hlout out - declare input1="${1} " input2="${2}" - input2="[ $input2 ]" - to_print="$((COLUMNS * 60 / 100))" - y=$(( to_print - ( ${#input1} + ${#input2} ) )) - hl="$(printf '%*s' $y '')" - hlout=${hl// /${symbol}} - out="${input1}${hlout}${input2}" - printf "%s\n" "${out}" -} - -# @description Trim given text to width of the terminal window. -# -# @example -# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." -# #Output -# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. -# -# @arg $1 string Text of first sentence. -# @arg $2 string Text of second sentence (optional). -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout trimmed text. -format::trim_text_to_term() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 - - declare to_print out input1="$1" input2="$2" - if [[ $# = 1 ]]; then - to_print="$((COLUMNS * 93 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; } - else - to_print="$((COLUMNS * 40 / 100))" - { [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; } - to_print="$((COLUMNS * 53 / 100))" - { [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; } - fi - printf "%s" "$out" -} - -__init diff --git a/src/configng/functions/bash-utility-master/src/interaction.sh b/src/configng/functions/bash-utility-master/src/interaction.sh deleted file mode 100644 index 910b60e1c..000000000 --- a/src/configng/functions/bash-utility-master/src/interaction.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# @file Interaction -# @brief Functions to enable interaction with the user. - -# @description Prompt yes or no question to the user. -# -# @example -# interaction::prompt_yes_no "Are you sure to proceed" "yes" -# #Output -# Are you sure to proceed (y/n)? [y] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer \[yes/no\] (optional). -# -# @exitcode 0 If user responds with yes. -# @exitcode 1 If user responds with no. -# @exitcode 2 Function missing arguments. -interaction::prompt_yes_no() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare def_arg response - def_arg="" - response="" - - case "${2}" in - [yY] | [yY][eE][sS]) - def_arg=y - ;; - [nN] | [nN][oO]) - def_arg=n - ;; - esac - - while :; do - printf "%s (y/n)? " "${1}" - [[ -n "${def_arg}" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -z "${response}" ]] && response="${def_arg}" - - case "${response}" in - [yY] | [yY][eE][sS]) - response=y - break - ;; - [nN] | [nN][oO]) - response=n - break - ;; - *) - response="" - ;; - esac - done - - [[ "${response}" = 'y' ]] && return 0 || return 1 -} - -# @description Prompt question to the user. -# -# @example -# interaction::prompt_response "Choose directory to install" "/home/path" -# #Output -# Choose directory to install? [/home/path] -# -# @arg $1 string The question to be prompted to the user. -# @arg $2 string default answer (optional). -# -# @exitcode 0 If user responds with answer. -# @exitcode 2 Function missing arguments. -# -# @stdout User entered answer to the question. -interaction::prompt_response() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare def_arg response - response="" - def_arg="${2}" - - while :; do - printf "%s ? " "${1}" - [[ -n "${def_arg}" ]] && [[ "${def_arg}" != "-" ]] && printf "[%s] " "${def_arg}" - - read -r response - [[ -n "${response}" ]] && break - - if [[ -z "${response}" ]] && [[ -n "${def_arg}" ]]; then - response="${def_arg}" - break - fi - done - - [[ "${response}" = "-" ]] && response="" - - printf "%s" "${response}" -} diff --git a/src/configng/functions/bash-utility-master/src/json.sh b/src/configng/functions/bash-utility-master/src/json.sh deleted file mode 100644 index 73476618e..000000000 --- a/src/configng/functions/bash-utility-master/src/json.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# @file Json -# @brief Simple json manipulation. These functions does not completely replace `jq` in any way. - -# @description Extract value from json based on key and position. -# Input to the function can be a pipe output, here-string or file. -# @example -# json::get_value "id" "1" < json_file -# json::get_value "id" <<< "${json_var}" -# echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" -# -# @arg $1 string id of the field to fetch. -# @arg $2 int position of value to extract.Defaults to 1.(optional) -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string value of extracted key. -json::get_value() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare LC_ALL=C num="${2:-1}" - grep -o "\"""${1}""\"\:.*" | sed -e "s/.*\"""${1}""\": //" -e 's/[",]*$//' -e 's/["]*$//' -e 's/[,]*$//' -e "s/\"//" -n -e "${num}"p -} diff --git a/src/configng/functions/bash-utility-master/src/misc.sh b/src/configng/functions/bash-utility-master/src/misc.sh deleted file mode 100644 index fca9a75bf..000000000 --- a/src/configng/functions/bash-utility-master/src/misc.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash - -# @file Miscellaneous -# @brief Set of miscellaneous helper functions. - -# @internal -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -_is_terminal() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Check if internet connection is available. -# -# @example -# misc::check_internet_connection -# -# @noargs -# -# @exitcode 0 If script can connect to internet. -# @exitcode 1 If script cannot access internet. -misc::check_internet_connection() { - declare check_internet - if _is_terminal; then - check_internet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" - else - check_internet="$(curl --compressed -Is google.com -m 10)" - fi - if [[ -z ${check_internet} ]]; then - return 1 - fi -} - -# @description Get list of process ids based on process name. -# -# @example -# misc::get_pid "chrome" -# #Ouput -# 25951 -# 26043 -# 26528 -# 26561 -# -# @arg $1 Name of the process to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout list of process ids. -misc::get_pid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - pgrep "${1}" -} - -# @description Get user id based on username. -# -# @example -# misc::get_uid "labbots" -# #Ouput -# 1000 -# -# @arg $1 username to search. -# -# @exitcode 0 If match successful. -# @exitcode 2 Function missing arguments. -# -# @stdout string uid for the username. -misc::get_uid() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - user_id=$(id "${1}" 2> /dev/null) - declare -i ret=$? - if [[ $ret -ne 0 ]]; then - printf "No user found with username: %s" "${1}\n" - return 1 - fi - - printf "%s\n" "${user_id}" | sed -e 's/(.*$//' -e 's/^uid=//' - - unset user_id -} - -# @description Generate random uuid. -# -# @example -# misc::generate_uuid -# #Ouput -# 65bc64d1-d355-4ffc-a9d9-dc4f3954c34c -# -# @noargs -# -# @exitcode 0 If match successful. -# -# @stdout random generated uuid. -misc::generate_uuid() { - C="89ab" - - for ((N=0;N<16;++N)); do - B="$((RANDOM%256))" - - case "$N" in - 6) printf '4%x' "$((B%16))" ;; - 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; - - 3|5|7|9) - printf '%02x-' "$B" - ;; - - *) - printf '%02x' "$B" - ;; - esac - done - - printf '\n' -} diff --git a/src/configng/functions/bash-utility-master/src/os.sh b/src/configng/functions/bash-utility-master/src/os.sh deleted file mode 100644 index 78e0d36c5..000000000 --- a/src/configng/functions/bash-utility-master/src/os.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env bash - -# @file Operating System -# @brief Functions to detect Operating system and version. - -# @description Identify the OS the function is run on. -# -# @noargs -# -# @example -# os::detect_os -# #Output -# linux -# -# @exitcode 0 If OS is successfully detected. -# @exitcode 1 If unable to detect OS. -# -# @stdout Operating system name (linux, mac or windows). -os::detect_os() { - declare uname os - uname=$(command -v uname) - - case $("${uname}" | tr '[:upper:]' '[:lower:]') in - linux*) - os="linux" - ;; - darwin*) - os="mac" - ;; - msys* | cygwin* | mingw* | nt | win*) - # or possible 'bash on windows' - os="windows" - ;; - *) - return 1 - ;; - esac - printf "%s" "${os}" -} - -# @description Identify the distribution flavour of linux. -# -# @noargs -# -# @example -# os::detect_linux_distro -# #Output -# ubuntu -# @exitcode 0 If Linux distro is successfully detected. -# @exitcode 1 If unable to detect OS distro. -# -# @stdout Linux OS distribution name (ubuntu, debian, suse, etc.,). -os::detect_linux_distro() { - declare distro - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro="${NAME}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro=$(lsb_release -si) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro="${DISTRIB_ID}" - elif [[ -f /etc/debian_version ]]; then - # Older Debian/Ubuntu/etc. - distro="debian" - elif [[ -f /etc/SuSe-release ]]; then - # Older SuSE/etc. - distro="suse" - elif [[ -f /etc/redhat-release ]]; then - # Older Red Hat, CentOS, etc. - distro="redhat" - else - return 1 - fi - printf "%s" "${distro}" | tr '[:upper:]' '[:lower:]' -} - -# @description Identify the Linux version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 20.04 -# -# @exitcode 0 If Linux version is successfully detected. -# @exitcode 1 If unable to detect Linux version. -# -# @stdout Linux OS version number (18.04, 20.04, etc.,). -os::detect_linux_version() { - declare distro_version - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro_version="${VERSION_ID}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro_version=$(lsb_release -sr) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro_version="${DISTRIB_RELEASE}" - else - return 1 - fi - printf "%s" "${distro_version}" -} - -# @description Identify the Linux codename. -# -# @noargs -# -# @example -# os::detect_linux_codename -# #Output -# jammy -# -# @exitcode 0 If Linux codename is successfully detected. -# @exitcode 1 If unable to detect Linux codename. -# -# @stdout Linux OS codename number (buster, jammy, etc.,). -os::detect_linux_codename() { - declare distro_codename - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . "/etc/os-release" - distro_codename="${VERSION_CODENAME}" - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - distro_codename=$(lsb_release -cs) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - distro_codename="${DISTRIB_CODENAME}" - else - return 1 - fi - printf "%s" "${distro_codename}" -} - -# @description Identify the MacOS version. -# -# @noargs -# -# @example -# os::detect_linux_version -# #Output -# 10.15.7 -# @exitcode 0 If MacOS version is successfully detected. -# @exitcode 1 If unable to detect MacOS version. -# -# @stdout MacOS version number (10.15.6, etc.,) -os::detect_mac_version() { - if [[ "$(os::detect_os)" = "mac" ]]; then - declare mac_version - mac_version="$(sw_vers -productVersion)" - printf "%s" "${mac_version}" - else - return 1 - fi -} diff --git a/src/configng/functions/bash-utility-master/src/string.sh b/src/configng/functions/bash-utility-master/src/string.sh deleted file mode 100644 index aa522cb55..000000000 --- a/src/configng/functions/bash-utility-master/src/string.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env bash - -# @file String -# @brief Functions for string operations and manipulations. - -# @description Strip whitespace from the beginning and end of a string. -# -# @example -# echo "$(string::trim " Hello World! ")" -# #Output -# Hello World! -# -# @arg $1 string The string to be trimmed. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout The trimmed string. -string::trim() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - : "${1#"${1%%[![:space:]]*}"}" - : "${_%"${_##*[![:space:]]}"}" - printf '%s\n' "$_" -} - -# @description Split a string to array by a delimiter. -# -# @example -# array=( $(string::split "a,b,c" ",") ) -# printf "%s" "$(string::split "Hello!World" "!")" -# #Output -# Hello -# World -# -# @arg $1 string The input string. -# @arg $2 string The delimiter string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns an array of strings created by splitting the string parameter by the delimiter. -string::split() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare -a arr=() - IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" - printf '%s\n' "${arr[@]}" -} - -# @description Strip characters from the beginning of a string. -# -# @example -# echo "$(string::lstrip "Hello World!" "He")" -# #Output -# llo World! -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::lstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1##$2}" -} - -# @description Strip characters from the end of a string. -# -# @example -# echo "$(string::rstrip "Hello World!" "d!")" -# #Output -# Hello Worl -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::rstrip() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - printf '%s\n' "${1%%$2}" -} - -# @description Make a string lowercase. -# -# @example -# echo "$(string::to_lower "HellO")" -# #Output -# hello -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the lowercased string. -string::to_lower() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1,,}" - else - printf "%s\n" "${@}" | tr '[:upper:]' '[:lower:]' - fi -} - -# @description Make a string all uppercase. -# -# @example -# echo "$(string::to_upper "HellO")" -# #Output -# HELLO -# -# @arg $1 string The input string. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the uppercased string. -string::to_upper() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then - printf '%s\n' "${1^^}" - else - printf "%s\n" "${@}" | tr '[:lower:]' '[:upper:]' - fi -} - -# @description Check whether the search string exists within the input string. -# -# @example -# string::contains "Hello World!" "lo" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::contains() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_contains hello he - [[ "${1}" == *${2}* ]] -} - -# @description Check whether the input string starts with key string. -# -# @example -# string::starts_with "Hello World!" "He" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::starts_with() { - # Usage: string_starts_with hello he - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - [[ "${1}" == ${2}* ]] -} - -# @description Check whether the input string ends with key string. -# -# @example -# string::ends_with "Hello World!" "d!" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::ends_with() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Usage: string_ends_wit hello lo - [[ "${1}" == *${2} ]] -} - -# @description Check whether the input string matches the given regex. -# -# @example -# string::regex "HELLO" "^[A-Z]*$" -# -# @arg $1 string The input string. -# @arg $2 string The search key. -# @exitcode 0 If match found. -# @exitcode 1 If no match found. -# @exitcode 2 Function missing arguments. -string::regex() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - if [[ ${1} =~ ${2} ]]; then - return 0 - else - return 1 - fi - -} diff --git a/src/configng/functions/bash-utility-master/src/terminal.sh b/src/configng/functions/bash-utility-master/src/terminal.sh deleted file mode 100644 index d73331d7a..000000000 --- a/src/configng/functions/bash-utility-master/src/terminal.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# @file Terminal -# @brief Set of useful terminal functions. - -# @description Check if script is run in terminal. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -terminal::is_term() { - [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 -} - -# @description Detect profile rc file for zsh and bash of current script running user. -# -# @noargs -# -# @exitcode 0 If script is run on terminal. -# @exitcode 1 If script is not run on terminal. -# -# @stdout path to the profile file. -terminal::detect_profile() { - declare CURRENT_SHELL="${SHELL##*/}" - case "${CURRENT_SHELL}" in - 'bash') DETECTED_PROFILE="${HOME}/.bashrc" ;; - 'zsh') DETECTED_PROFILE="${HOME}/.zshrc" ;; - *) if [[ -f "${HOME}/.profile" ]]; then - DETECTED_PROFILE="${HOME}/.profile" - else - printf "No compaitable shell file\n" && exit 1 - fi ;; - esac - printf "%s\n" "${DETECTED_PROFILE}" -} - -# @description Clear the output in terminal on the specified line number. -# This function clears line only on terminal. -# -# @arg $1 Line number to clear. Defaults to 1. (optional) -# -# @exitcode 0 If script is run on terminal. -# -# @stdout clear line ansi code. -terminal::clear_line() { - if terminal::is_term; then - declare line=${1:-1} - printf "\033[%sA\033[2K" "${line}" - fi -} diff --git a/src/configng/functions/bash-utility-master/src/validation.sh b/src/configng/functions/bash-utility-master/src/validation.sh deleted file mode 100644 index 37fde1cbc..000000000 --- a/src/configng/functions/bash-utility-master/src/validation.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env bash - -# @file Validation -# @brief Functions to perform validation on given data. - -# @description Validate whether a given input is a valid email address or not. -# -# @example -# test='test@gmail.com' -# validation::email "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string input email address to validate. -# -# @exitcode 0 If provided input is an email address. -# @exitcode 1 If provided input is not an email address. -# @exitcode 2 Function missing arguments. -validation::email() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare email_re - email_re="^([A-Za-z]+[A-Za-z0-9]*\+?((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*)*)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" - [[ "${1}" =~ ${email_re} ]] && return 0 || return 1 -} - -# @description Validate whether a given input is a valid IP V4 address. -# -# @example -# ips=' -# 4.2.2.2 -# a.b.c.d -# 192.168.1.1 -# 0.0.0.0 -# 255.255.255.255 -# 255.255.255.256 -# 192.168.0.1 -# 192.168.0 -# 1234.123.123.123 -# 0.192.168.1 -# ' -# for ip in $ips; do -# if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi -# printf "%-20s: %s\n" "$ip" "$stat" -# done -# #Output -# 4.2.2.2 : good -# a.b.c.d : bad -# 192.168.1.1 : good -# 0.0.0.0 : good -# 255.255.255.255 : good -# 255.255.255.256 : bad -# 192.168.0.1 : good -# 192.168.0 : bad -# 1234.123.123.123 : bad -# 0.192.168.1 : good -# -# @arg $1 string input IPv4 address. -# -# @exitcode 0 If provided input is a valid IPv4. -# @exitcode 1 If provided input is not a valid IPv4. -# @exitcode 2 Function missing arguments. -validation::ipv4() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - declare ip="${1}" - declare IFS=. - # shellcheck disable=SC2206 - declare -a a=($ip) - [[ "${ip}" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 - # Test values of quads - declare quad - for quad in {0..3}; do - [[ "${a[$quad]}" -gt 255 ]] && return 1 - done - return 0 -} - -# @description Validate whether a given input is a valid IP V6 address. -# -# @example -# ips=' -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 -# fe80::1ff:fe23:4567:890a -# fe80::1ff:fe23:4567:890a%eth2 -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar -# fezy::1ff:fe23:4567:890a -# :: -# 2001:db8:: -# ' -# for ip in $ips; do -# if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi -# printf "%-50s= %s\n" "$ip" "$stat" -# done -# #Output -# 2001:db8:85a3:8d3:1319:8a2e:370:7348 = good -# fe80::1ff:fe23:4567:890a = good -# fe80::1ff:fe23:4567:890a%eth2 = good -# 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad -# fezy::1ff:fe23:4567:890a = bad -# :: = good -# 2001:db8:: = good -# -# @arg $1 string input IPv6 address. -# -# @exitcode 0 If provided input is a valid IPv6. -# @exitcode 1 If provided input is not a valid IPv6. -# @exitcode 2 Function missing arguments. -validation::ipv6() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare ip="${1}" - declare 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]))$" - - [[ "${ip}" =~ $re ]] && return 0 || return 1 -} - -# @description Validate if given variable is entirely alphabetic characters. -# -# @example -# test='abcABC' -# validation::alpha "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is only alpha characters. -# @exitcode 1 If input contains any non alpha characters. -# @exitcode 2 Function missing arguments. -validation::alpha() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable contains only alpha-numeric characters. -# -# @example -# test='abc123' -# validation::alpha_num "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is an alpha-numeric. -# @exitcode 1 If input is not an alpha-numeric. -# @exitcode 2 Function missing arguments. -validation::alpha_num() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alnum:]]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. -# -# @example -# test='abc-ABC_cD' -# validation::alpha_dash "${test}" -# echo $? -# #Output -# 0 -# -# @arg $1 string Value of variable to validate. -# -# @exitcode 0 If input is valid. -# @exitcode 1 If input the input is not valid. -# @exitcode 2 Function missing arguments. -validation::alpha_dash() { - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - - declare re='^[[:alpha:]_-]+$' - if [[ "${1}" =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Compares version numbers and provides return based on whether the value in equal, less than or greater. -# -# @arg $1 string Version number to check (eg: 1.0.1) -# $arg $2 string Version number to check (eg: 1.0.1) -# -# @example -# test='abc-ABC_cD' -# validation::version_comparison "12.0.1" "12.0.1" -# echo $? -# #Output -# 0 -# -# @exitcode 0 version number is equal. -# @exitcode 1 $1 version number is greater than $2. -# @exitcode 2 $1 version number is less than $2. -# @exitcode 3 Function is missing required arguments. -# @exitcode 4 Provided input argument is in invalid format. -validation::version_comparison() { - [[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 3 - - declare regex="^[.0-9]*$" - ! [[ $1 =~ $regex ]] && printf "Invalid argument: %s \n" "${1}" && return 4 - ! [[ $2 =~ $regex ]] && printf "Invalid argument invalid: %s \n" "${2}" && return 4 - - if [[ "$1" == "$2" ]]; then - return 0 - fi - declare IFS=. - declare -a ver1 ver2 - read -r -a ver1 <<<"${1}" - read -r -a ver2 <<<"${2}" - # fill empty fields in ver1 with zeros - for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do - ver1[i]=0 - done - for ((i = 0; i < ${#ver1[@]}; i++)); do - if [[ -z ${ver2[i]} ]]; then - # fill empty fields in ver2 with zeros - ver2[i]=0 - fi - if ((10#${ver1[i]} > 10#${ver2[i]})); then - return 1 - fi - if ((10#${ver1[i]} < 10#${ver2[i]})); then - return 2 - fi - done - return 0 -} diff --git a/src/configng/functions/bash-utility-master/src/variable.sh b/src/configng/functions/bash-utility-master/src/variable.sh deleted file mode 100644 index 67e9fab55..000000000 --- a/src/configng/functions/bash-utility-master/src/variable.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash - -# @file Variable -# @brief Functions for handling variables. - -# @description Check if given variable is array. -# Pass the variable name instead of value of the variable. -# -# @example -# arr=("a" "b" "c") -# variable::is_array "arr" -# #Output -# 0 -# -# @arg $1 string name of the variable to check. -# -# @exitcode 0 If input is array. -# @exitcode 1 If input is not an array. -variable::is_array() { - if [[ -z "${1}" ]]; then - return 1 - else - declare -p "${1}" 2> /dev/null | grep 'declare \-[aA]' > /dev/null && return 0 - fi - return 1 -} - -# @description Check if given variable is a number. -# -# @example -# variable::is_numeric "1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is number. -# @exitcode 1 If input is not a number. -variable::is_numeric() { - declare re='^[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is an integer. -# -# @example -# variable::is_int "+1234" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is an integer. -# @exitcode 1 If input is not an integer. -variable::is_int() { - declare re='^[+-]?[0-9]+$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a float. -# -# @example -# variable::is_float "+1234.0" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a float. -# @exitcode 1 If input is not a float. -variable::is_float() { - declare re='^[+-]?[0-9]+.?[0-9]*$' - if [[ ${1} =~ $re ]]; then - return 0 - fi - return 1 -} - -# @description Check if given variable is a boolean. -# -# @example -# variable::is_bool "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is a boolean. -# @exitcode 1 If input is not a boolean. -variable::is_bool() { - [[ "${1}" = true || "${1}" = false ]] && return 0 || return 1 -} - -# @description Check if given variable is a true. -# -# @example -# variable::is_true "true" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is true. -# @exitcode 1 If input is not true. -variable::is_true() { - [[ "${1}" = true || "${1}" -eq 0 ]] && return 0 || return 1 -} - -# @description Check if given variable is false. -# -# @example -# variable::is_false "false" -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is false. -# @exitcode 1 If input is not false. -variable::is_false() { - [[ "${1}" = false || "${1}" -eq 1 ]] && return 0 || return 1 -} - -# @description Check if given variable is empty or null. -# -# @example -# test='' -# variable::is_empty_or_null $test -# #Output -# 0 -# -# @arg $1 mixed Value of variable to check. -# -# @exitcode 0 If input is empty or null. -# @exitcode 1 If input is not empty or null. -variable::is_empty_or_null() { - [[ -z "${1}" || "${1}" = "null" ]] && return 0 || return 1 -} diff --git a/src/configng/functions/cpu.sh b/src/configng/functions/cpu.sh deleted file mode 100644 index 838c6fdbe..000000000 --- a/src/configng/functions/cpu.sh +++ /dev/null @@ -1,199 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. -# - -# @description Return policy as int based on original armbian-config logic. -# -# @example -# cpu::get_policy -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -# -# @stdout Policy as integer. -cpu::get_policy(){ - declare -i policy=0 - [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 - [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 - [[ -d /sys/devices/system/cpu/cpufreq/policy0 && -d /sys/devices/system/cpu/cpufreq/policy2 ]] && policy=2 - printf '%d\n' "$policy" -} - -# @description Return CPU frequencies as string delimited by space. -# -# @example -# cpu::get_freqs 0 -# echo $? -# #Output -# 648000 816000 912000 960000 1008000 1056000 1104000 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout Space delimited string of CPU frequencies. -cpu::get_freqs(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_frequencies" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU minimum frequency as string. -# -# @example -# cpu::get_min_freq 0 -# echo $? -# #Output -# 648000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU minimum frequency as string. -cpu::get_min_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_min_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU maximum frequency as string. -# -# @example -# cpu::get_max_freq 0 -# echo $? -# #Output -# 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU maximum frequency as string. -cpu::get_max_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_max_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governor as string. -# -# @example -# cpu::get_governor 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::get_governor(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_governor" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governors as string delimited by space. -# -# @example -# cpu::get_governors 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::get_governors(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_governors" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Set min, max and CPU governor. -# -# @example -# cpu::set_freq 0 648000 1152000 performance -# echo $? -# #Output -# performance -# -# @arg $1 int Policy. -# @arg $2 int Minimum frequency. -# @arg $3 int Maximum frequency. -# @arg $4 string Governor. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode 3 Invalid minimum frequency. -# @exitcode 4 Invalid maximum frequency. -# @exitcode 5 Minimum frequency must be <= maximum frequency. -# @exitcode 6 Invalid governor. -cpu::set_freq(){ - # Check number of arguments - [[ $# -lt 4 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/etc/default/cpufrequtils" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - declare -i policy=$1 - declare -i min_freq=$2 - declare -i max_freq=$3 - local governor=$4 - # Return frequencies as array - declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) - # Validate minimum frequency - array::contains "$min_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 3 - # Validate maximum frequency - array::contains "$max_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid maximum frequency\n" "${FUNCNAME[0]}" && return 4 - # Validate minimum frequency is <= maximum frequency - [ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 - # Return governors - declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) - # Validate maximum governor - array::contains "$governor" ${governor[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 6 - # Update file - sed -i "s/MIN_SPEED=.*/MIN_SPEED=$min_freq/" "$file" - sed -i "s/MAX_SPEED=.*/MAX_SPEED=$max_freq/" "$file" - sed -i "s/GOVERNOR=.*/GOVERNOR=$governor/" "$file" - # Return value - return 0 -} diff --git a/src/configng/test/cpu_test.sh b/src/configng/test/cpu_test.sh deleted file mode 100644 index f66aecb9d..000000000 --- a/src/configng/test/cpu_test.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related tests. -# - -cur_dir=$(pwd) -# Source bash-utility and cpu functions (this will word under sudo) -source "$cur_dir/../functions/bash-utility-master/src/string.sh" -source "$cur_dir/../functions/bash-utility-master/src/collection.sh" -source "$cur_dir/../functions/bash-utility-master/src/array.sh" -source "$cur_dir/../functions/bash-utility-master/src/check.sh" -source "$cur_dir/../functions/cpu.sh" - -# @description Print value from collection. -# -# @example -# collection::each "print_func" -# #Output -# Value in collection -print_func(){ - printf "%s\n" "$1" - return 0 - } - -# @description Check function exit code and exit script if != 0. -# -# @example -# check_return -# #Output -# Nothing -check_return(){ - if [ "$?" -ne 0 ]; then - exit 1 - fi - } - -# Get policy -declare -i policy=$(cpu::get_policy) -printf 'Policy = %d\n' "$policy" -declare -i min_freq=$(cpu::get_min_freq $policy) -check_return -printf 'Minimum frequency = %d\n' "$min_freq" -declare -i max_freq=$(cpu::get_max_freq $policy) -check_return -printf 'Maximum frequency = %d\n' "$max_freq" -governor=$(cpu::get_governor $policy) -check_return -printf 'Governor = %s\n' "$governor" -# Return frequencies as array -declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) -check_return -printf "\nAll frequencies\n" -# Print all values in collection -printf "%s\n" "${freqs[@]}" | collection::each "print_func" -declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) -check_return -printf "\nAll governors\n" -# Print all values in collection -printf "%s\n" "${governors[@]}" | collection::each "print_func" -# Are we running as sudo? -[[ "$EUID" != 0 ]] && printf "Must call cpu::set_freq as sudo\n" && exit 1 -# Before -printf "\nBefore\n" -cat /etc/default/cpufrequtils -cpu::set_freq $policy "$min_freq" "$max_freq" performance -# After -printf "\nAfter\n" -cat /etc/default/cpufrequtils - From 09202c7c28220cfbeb1b5fd96db4b99a9073abda Mon Sep 17 00:00:00 2001 From: Joseph Turner Date: Sun, 16 Jul 2023 12:47:49 -0700 Subject: [PATCH 21/48] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 325303253..700c4e754 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,22 @@ may not include Python, C/C++, etc. build/runtime environments * `sudo apt install git` * `cd ~/` * `git clone https://github.com/armbian/configng.git` -* `sudo ~/configng/configng.sh` +* `sudo ~/configng/config.sh` #### If all goes well you should see list or avalible commands ``` -Usage: configng [ -h | foo ] +Usage: config [ -h | foo ] Options: -h) Print this help. - foo) Usage: configng foo [ boardled::options ][ cpu::options ][ extra_drive::options ][ benchymark::options ]:: + foo) Usage: config foo [ boardled::options ][ cpu::options ][ extra_drive::options ][ benchymark::options ]:: boardled::options - set_sysled_cpu set the Sys board led to montor cpu activity. - set_sysled_none set the Sys board led to montor none. see_sysled See a list of board led options. + see_sysled_none Set board led options to none (off). + see_sysled_cpu Set board led options to monitor CPU. + see_sysled_beat Set board led options to heartbeat pulse. cpu::options see_policy Return policy as int based on original armbian-config logic. @@ -32,7 +33,7 @@ Options: see_max_freq Return CPU maximum frequency as string. see_governor Return CPU governor as string. see_governors Return CPU governors as string delimited by space. - set_freq Set min, max and CPU governor. + set_freq Set min, max and CPU governor. "Disabled " extra_drive::options set_spi_vflash Set up a simulated MTD spi flash for testing. @@ -40,8 +41,7 @@ Options: benchymark::options see_monitor system boot-up performance statistics. - see_boot_blame system boot-up performance statistics. - see_7ZipBench 7-zip benchmark based on original armbianmonitor logic. + see_boot_times system boot-up performance statistics. ``` From 37fa45d30f247cf3cdc43c7f3d69bdfb025947fb Mon Sep 17 00:00:00 2001 From: Joseph Turner Date: Sun, 16 Jul 2023 13:02:08 -0700 Subject: [PATCH 22/48] Update README.md --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 700c4e754..55db1ba33 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ may not include Python, C/C++, etc. build/runtime environments * `sudo apt install git` * `cd ~/` * `git clone https://github.com/armbian/configng.git` -* `sudo ~/configng/config.sh` +* `~/configng/config.sh` #### If all goes well you should see list or avalible commands ``` @@ -44,10 +44,27 @@ Options: see_boot_times system boot-up performance statistics. ``` +#### Change the systems led to pulse a hearbeat +``` + bash ~/configng/bin/config.sh foo boardled::see_sysled_beat +``` +#### Change the systems led to off +``` + bash ~/configng/bin/config.sh foo boardled::see_sysled_none +``` +#### See avalible settings sytem led options and current setting in [] +``` + bash ~/configng/bin/config.sh foo boardled::see_sysled +``` +#### See avalible armbian monitor options +``` + bash ~/configng/bin/config.sh foo benchymark:see_monitor +``` + ## Coding standards [Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, -but fundementally look at the code in Bash Utility: +but fundementally look at the code in lib: ``` # @description Strip characters from the beginning of a string. # From 8545f4d17e7b021f45e0a7c8f6ba36c91a431085 Mon Sep 17 00:00:00 2001 From: Joseph Turner Date: Sun, 16 Jul 2023 15:47:11 -0700 Subject: [PATCH 23/48] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 55db1ba33..7e2de5280 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ may not include Python, C/C++, etc. build/runtime environments * `sudo apt install git` * `cd ~/` * `git clone https://github.com/armbian/configng.git` -* `~/configng/config.sh` +* `bash ~/configng/config.sh` #### If all goes well you should see list or avalible commands ``` @@ -48,9 +48,9 @@ Options: ``` bash ~/configng/bin/config.sh foo boardled::see_sysled_beat ``` -#### Change the systems led to off +#### Change the systems led to off show a result in whiptail or dialog if installed ``` - bash ~/configng/bin/config.sh foo boardled::see_sysled_none + bash ~/configng/bin/config.sh foo boardled::see_sysled_none | bash ~/configng/bin/jampi-config.sh ``` #### See avalible settings sytem led options and current setting in [] ``` From 51b86d39938f54b75b07a72a4fa53d9d58059bf6 Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 15:49:02 -0700 Subject: [PATCH 24/48] modified: bin/jampi-config --- bin/config.sh | 0 bin/jampi-config | 362 ++++++++++++++++++++++++----------------------- 2 files changed, 187 insertions(+), 175 deletions(-) mode change 100644 => 100755 bin/config.sh mode change 100644 => 100755 bin/jampi-config diff --git a/bin/config.sh b/bin/config.sh old mode 100644 new mode 100755 diff --git a/bin/jampi-config b/bin/jampi-config old mode 100644 new mode 100755 index 4dd17d7c6..f9f64e34b --- a/bin/jampi-config +++ b/bin/jampi-config @@ -35,178 +35,190 @@ # POSSIBILITY OF SUCH DAMAGE. # - - -#DIRECTORY variable to the absolute path of the script's directory -#directory=$(cd "$(dirname "$0")" && pwd) -directory="$(dirname "$(readlink -f "$0")")" -filename=$(basename "${BASH_SOURCE[0]}") -selfpath="$directory"/"$filename" -#libpath="${directory}"/"its-lib" #Include these scripts Library -libpath="$selfpath" - - -clear -# Check for the availability of whiptail and dialog command-line programs -# Set the default program to use for displaying messages -( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" -( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" -( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" - -# if both whiptail and dialog change to prefered -( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" - - -[[ "$1" == -b ]] && default="bash" -[[ "$1" == -w ]] && default="whiptail" -[[ "$1" == -n ]] && default="dialog" - -[[ "$1" == -m ]] && { -export NEWT_COLORS=" -root=blue,black -border=green,black -title=green,black -roottext=red,black -window=red,black -textbox=white,black -button=black,green -compactbutton=white,black -listbox=white,black -actlistbox=black,white -actsellistbox=black,green -checkbox=green,black -actcheckbox=black,green -" -} - -# Check the cpu architecture. for later handeling if nessery -architecture=$(dpkg --print-architecture) -[[ "$architecture" == "armf" ]] && true ; - -#setup menu arrays -readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) -readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) -readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) - -## System@Settings:Advanced Settings (armbian-config) -cmd_advance() -{ - sudo armbian-config -} - -## System@CPU info:Example from Bash Utility (cpu_test.sh) -cmd_cpu_info() -{ - [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; - [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - - -## System@CPU info:An example function (lscpu) -cmd_cpu_ls() -{ - - shell_command="$(lscpu)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - -## System@Bootup Time:An example function (systemd-analyze time) -cmd_boot_time() -{ - - shell_command="$(systemd-analyze time)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - -## System@Login Info :An example function (Login Info) -cmd_lslogins() -{ - - shell_command="$(lslogins)" ; - message_box=$( printf '%s ' "${shell_command[@]}" ) ; - see_message -} - - -# Function to display a message using whiptail, dialog or printf depending on what is available on the system -see_message() -{ - # Use if neither whiptail nor dialog are available on the system - if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then - # Use printf to display the message - printf '%s ' "${shell_command[@]}" - - # Use as default if whiptail is available - elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then - # Use whiptail to display the message - whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext - - # Use if dialog is available on the system but not whiptail - elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then - # Use dialog to display the message - dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear - fi -} - -see_menu() -{ - - if [[ "$default" == "bash" ]]; then - PS3="Enter a number: " - select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done - - elif [[ "$default" == "whiptail" ]]; then - OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) - [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" - - elif [[ "$default" == "dialog" ]]; then - OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) - [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" - fi -} - -see_help(){ - - echo "" - echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" - echo -e "Options:" - echo -e " -h Print this help." - echo -e " -b GNU bash " - echo -e " -n NCURSES dialog " - echo -e " -w NEWT whiptail - default colors " - echo -e " -m dark mode whiptail " - echo -e " -dev Options:" - for i in "${!functionarray[@]}"; do - printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" - done - - } - -main() -{ - if [[ "$1" == --dev ]] ; then - default="bash" - local found=false - for i in "${!functionarray[@]}"; do - if [ "$2" == "${functionarray[i]}" ]; then - "${functionarray[i]}" - found=true - break - fi - done - if ! $found; then - see_help - exit 0 - fi - elif [[ "$1" == -h ]] ; then - see_help - else - see_menu - fi -} - -main "$@" +# Comeback to this later +# +##DIRECTORY variable to the absolute path of the script's directory +##directory=$(cd "$(dirname "$0")" && pwd) +#directory="$(dirname "$(readlink -f "$0")")" +#filename=$(basename "${BASH_SOURCE[0]}") +#selfpath="$directory"/"$filename" +##libpath="${directory}"/"its-lib" #Include these scripts Library +#libpath="$selfpath" +# +# +#clear +## Check for the availability of whiptail and dialog command-line programs +## Set the default program to use for displaying messages +#( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" +#( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" +#( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" +# +## if both whiptail and dialog change to prefered +#( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" +# +# +#[[ "$1" == -b ]] && default="bash" +#[[ "$1" == -w ]] && default="whiptail" +#[[ "$1" == -n ]] && default="dialog" +# +#[[ "$1" == -m ]] && { +#export NEWT_COLORS=" +#root=blue,black +#border=green,black +#title=green,black +#roottext=red,black +#window=red,black +#textbox=white,black +#button=black,green +#compactbutton=white,black +#listbox=white,black +#actlistbox=black,white +#actsellistbox=black,green +#checkbox=green,black +#actcheckbox=black,green +#" +#} +# +## Check the cpu architecture. for later handeling if nessery +#architecture=$(dpkg --print-architecture) +#[[ "$architecture" == "armf" ]] && true ; +# +##setup menu arrays +#readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) +#readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) +#readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) +# +### System@Settings:Advanced Settings (armbian-config) +#cmd_advance() +#{ +# sudo armbian-config +#} +# +### System@CPU info:Example from Bash Utility (cpu_test.sh) +#cmd_cpu_info() +#{ +# [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; +# [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) +# message_box=$( printf '%s ' "${shell_command[@]}" ) ; +# see_message +#} +# +# +### System@CPU info:An example function (lscpu) +#cmd_cpu_ls() +#{ +# +# shell_command="$(lscpu)" ; +# message_box=$( printf '%s ' "${shell_command[@]}" ) ; +# see_message +#} +# +### System@Bootup Time:An example function (systemd-analyze time) +#cmd_boot_time() +#{ +# +# shell_command="$(systemd-analyze time)" ; +# message_box=$( printf '%s ' "${shell_command[@]}" ) ; +# see_message +#} +# +### System@Login Info :An example function (Login Info) +#cmd_lslogins() +#{ +# +# shell_command="$(lslogins)" ; +# message_box=$( printf '%s ' "${shell_command[@]}" ) ; +# see_message +#} +# +# +## Function to display a message using whiptail, dialog or printf depending on what is available on the system +#see_message() +#{ +# # Use if neither whiptail nor dialog are available on the system +# if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then +# # Use printf to display the message +# printf '%s ' "${shell_command[@]}" +# +# # Use as default if whiptail is available +# elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then +# # Use whiptail to display the message +# whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext +# +# # Use if dialog is available on the system but not whiptail +# elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then +# # Use dialog to display the message +# dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear +# fi +#} +# +#see_menu() +#{ +# +# if [[ "$default" == "bash" ]]; then +# PS3="Enter a number: " +# select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done +# +# elif [[ "$default" == "whiptail" ]]; then +# OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) +# [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" +# +# elif [[ "$default" == "dialog" ]]; then +# OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) +# [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" +# fi +#} +# +#see_help(){ +# +# echo "" +# echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" +# echo -e "Options:" +# echo -e " -h Print this help." +# echo -e " -b GNU bash " +# echo -e " -n NCURSES dialog " +# echo -e " -w NEWT whiptail - default colors " +# echo -e " -m dark mode whiptail " +# echo -e " -dev Options:" +# for i in "${!functionarray[@]}"; do +# printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" +# done +# +# } +# +#main() +#{ +# if [[ "$1" == --dev ]] ; then +# default="bash" +# local found=false +# for i in "${!functionarray[@]}"; do +# if [ "$2" == "${functionarray[i]}" ]; then +# "${functionarray[i]}" +# found=true +# break +# fi +# done +# if ! $found; then +# see_help +# exit 0 +# fi +# elif [[ "$1" == -h ]] ; then +# see_help +# else +# see_menu +# fi +#} +# +#main "$@" + +if command -v whiptail > /dev/null; then + DIALOG=whiptail +elif command -v dialog > /dev/null; then + DIALOG=dialog +else + echo "Error: Neither whiptail nor dialog is installed." + exit 1 +fi + +input=$(cat) +$DIALOG --title "Config Options" --msgbox "$input" 0 0 From b5f5ef6926d2302af3a0f1acb28d394f58f06ccb Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 18:53:35 -0700 Subject: [PATCH 25/48] new file: lib/config/io.sh ir toggle arbian-config netwokr/ir --- lib/config/io.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lib/config/io.sh diff --git a/lib/config/io.sh b/lib/config/io.sh new file mode 100644 index 000000000..c08127704 --- /dev/null +++ b/lib/config/io.sh @@ -0,0 +1,17 @@ + +# @description Enable or Disable Infrared Remote Control. +# +# @example +# io::set_ir_toggle enable +# io::set_ir_toggle disable +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +io::set_ir_toggle(){ + +[[ "$1" == "enable" ]] && sudo apt -y --no-install-recommends install lirc ; +[[ "$1" == "disabe" ]] && sudo apt -y remove lirc ; sudo apt -y -qq autoremove ; + +} \ No newline at end of file From 6dc386eac5e2d26c4b1050df398017eb99bd2c33 Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 20:05:26 -0700 Subject: [PATCH 26/48] lib/config/desktops.sh --- lib/config/desk_setup.sh | 1 + lib/config/desktop_install.sh | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 lib/config/desk_setup.sh create mode 100644 lib/config/desktop_install.sh diff --git a/lib/config/desk_setup.sh b/lib/config/desk_setup.sh new file mode 100644 index 000000000..5df3bb9ff --- /dev/null +++ b/lib/config/desk_setup.sh @@ -0,0 +1 @@ +apt-cache search armbian-$(grep VERSION_CODENAME /etc/os-release | cut -d"=" -f2)-desktop- | cut -d" " -f1 \ No newline at end of file diff --git a/lib/config/desktop_install.sh b/lib/config/desktop_install.sh new file mode 100644 index 000000000..da8ce1e48 --- /dev/null +++ b/lib/config/desktop_install.sh @@ -0,0 +1,25 @@ +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. +# +# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. + +# @description Return policy as int based on original armbian-config logic. +# +# @example +# cpu::get_policy +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +# +# @stdout Policy as integer. +desk_setup::see_desktops(){ + + apt-cache search armbian-$(grep VERSION_CODENAME /etc/os-release | cut -d"=" -f2)-desktop- | cut -d" " -f1 + + } \ No newline at end of file From de5b9a121007fc9b2d2b966de673a16dc5563219 Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 20:06:07 -0700 Subject: [PATCH 27/48] lib/config/desktops.sh https://armbian.atlassian.net/browse/AR-978 --- lib/config/benchymark.sh | 16 ++++++---------- lib/config/desk_setup.sh | 1 - lib/config/{desktop_install.sh => desktops.sh} | 8 ++++---- 3 files changed, 10 insertions(+), 15 deletions(-) delete mode 100644 lib/config/desk_setup.sh rename lib/config/{desktop_install.sh => desktops.sh} (67%) diff --git a/lib/config/benchymark.sh b/lib/config/benchymark.sh index c99351b6a..1882c8812 100644 --- a/lib/config/benchymark.sh +++ b/lib/config/benchymark.sh @@ -7,17 +7,14 @@ # warranty of any kind, whether express or implied. # # Benchmark related functions. See -# https://systemd.io/ for more info. -# https://www.7-zip.org/ +# https://systemd.io/ https://www.7-zip.org/ for more info. # @description system boot-up performance statistics. # # @example -# benchymark::see_systemd $1 (blame time chain) +# benchymark::see_systemd $1 (-h) # #Output -# time (quick check of boot time) -# balme (List modual load times) -# chain () +# armbianmonitor help list options.) # # @exitcode 0 If successful. # @@ -41,11 +38,11 @@ benchymark::see_monitor(){ # @description system boot-up performance statistics. # # @example -# benchymark::see_systemd $1 (blame time chain) +# benchymark::see_systemd $1 (blame time) # #Output +# -h (systemd help list, not not all are avalible.) # time (quick check of boot time) -# balme (List modual load times) -# chain () +# balme (Lists modual boot load times) # # @exitcode 0 If successful. # @@ -55,7 +52,6 @@ benchymark::see_boot_times(){ [[ $1 == "" ]] && sys_blame=$( systemd-analyze -h ) ; [[ $1 == "blame" ]] && sys_blame=$( systemd-analyze blame ) ; [[ $1 == "time" ]] && sys_blame=$( systemd-analyze time ) ; - [[ $1 == "chain" ]] && sys_blame=$( systemd-analyze critical-chain ) ; printf '%s\n' "${sys_blame[@]}" exit 0 } diff --git a/lib/config/desk_setup.sh b/lib/config/desk_setup.sh deleted file mode 100644 index 5df3bb9ff..000000000 --- a/lib/config/desk_setup.sh +++ /dev/null @@ -1 +0,0 @@ -apt-cache search armbian-$(grep VERSION_CODENAME /etc/os-release | cut -d"=" -f2)-desktop- | cut -d" " -f1 \ No newline at end of file diff --git a/lib/config/desktop_install.sh b/lib/config/desktops.sh similarity index 67% rename from lib/config/desktop_install.sh rename to lib/config/desktops.sh index da8ce1e48..8024962d4 100644 --- a/lib/config/desktop_install.sh +++ b/lib/config/desktops.sh @@ -5,19 +5,19 @@ # License version 2. This program is licensed "as is" without any # warranty of any kind, whether express or implied. # -# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. +# Desktop setup related functions. See *(todo)* for more info. -# @description Return policy as int based on original armbian-config logic. +# @description Display a list of avalible desktops to install. # # @example -# cpu::get_policy +# desk_setup::see_desktops # echo $? # #Output # 0 # # @exitcode 0 If successful. # -# @stdout Policy as integer. +# @stdout list of avalible desktops. desk_setup::see_desktops(){ apt-cache search armbian-$(grep VERSION_CODENAME /etc/os-release | cut -d"=" -f2)-desktop- | cut -d" " -f1 From e6f0fcd10ceb0c528458ddfd195c651ae157832b Mon Sep 17 00:00:00 2001 From: Joseph Turner Date: Sun, 16 Jul 2023 21:08:19 -0700 Subject: [PATCH 28/48] Update README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e2de5280..bf66bb5c6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,13 @@ Usage: config [ -h | foo ] Options: -h) Print this help. - foo) Usage: config foo [ boardled::options ][ cpu::options ][ extra_drive::options ][ benchymark::options ]:: + foo) Usage: config foo [ desk_setup::options ][ io::options ][ boardled::options ][ cpu::options ][ extra_drive::options ][ benchymark::options ]:: + + desk_setup::options + see_desktops Display a list of avalible desktops to install. + + io::options + set_ir_toggle [ enable ][ disable ] Infrared Remote Control. boardled::options see_sysled See a list of board led options. @@ -32,8 +38,8 @@ Options: see_min_freq Return CPU minimum frequency as string. see_max_freq Return CPU maximum frequency as string. see_governor Return CPU governor as string. - see_governors Return CPU governors as string delimited by space. - set_freq Set min, max and CPU governor. "Disabled " + see_governors Return CPU avalible governors as string delimited by space. + set_freq ** disabled ** Set min, max and CPU governor. extra_drive::options set_spi_vflash Set up a simulated MTD spi flash for testing. From 2128c708c7c97226753b69536baf2d4c134de67a Mon Sep 17 00:00:00 2001 From: tearran Date: Sun, 16 Jul 2023 22:51:20 -0700 Subject: [PATCH 29/48] renamed: lib/config/board_led.sh -> lib/config/boardled.sh renamed: lib/config/extra_drives.sh -> lib/config/extradrives.sh edits comments and renames --- lib/config/{board_led.sh => boardled.sh} | 2 +- lib/config/{extra_drives.sh => extradrives.sh} | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename lib/config/{board_led.sh => boardled.sh} (98%) rename lib/config/{extra_drives.sh => extradrives.sh} (93%) diff --git a/lib/config/board_led.sh b/lib/config/boardled.sh similarity index 98% rename from lib/config/board_led.sh rename to lib/config/boardled.sh index 473f5d890..c8eb58b64 100644 --- a/lib/config/board_led.sh +++ b/lib/config/boardled.sh @@ -12,7 +12,7 @@ # @description See a list of board led options. # # @example -# boardLED::set_sysled +# boardled::set_sysled # #Output # Led blinks to set $1 # diff --git a/lib/config/extra_drives.sh b/lib/config/extradrives.sh similarity index 93% rename from lib/config/extra_drives.sh rename to lib/config/extradrives.sh index 771489f86..dad7a0639 100644 --- a/lib/config/extra_drives.sh +++ b/lib/config/extradrives.sh @@ -13,7 +13,7 @@ # @description Set up a simulated MTD spi flash for testing. # # @example -# extra_drive::set_spi_vflash +# extradrives::set_spi_vflash # echo $? # #Output # /dev/mtd0 @@ -21,7 +21,7 @@ # /dev/mtdblock0 # # @exitcode 0 If successful. -extra_drive::set_spi_vflash(){ +extradrives::set_spi_vflash(){ # Load the nandsim and mtdblock modules to create a virtual MTD device @@ -58,13 +58,13 @@ extra_drive::set_spi_vflash(){ # @description Remove tsting simulated MTD spi flash. # # @example -# extra_drive::rem_spi_vflash +# extradrives::rem_spi_vflash # echo $? # #Output # 0 # # @exitcode 0 If successful. -extra_drive::rem_spi_vflash(){ +extradrives::rem_spi_vflash(){ # Unmount the virtual MTD device from the mount point umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') From e1da6c78ed24c8113e5fe274046da6ba14717e89 Mon Sep 17 00:00:00 2001 From: tearran Date: Mon, 17 Jul 2023 00:18:35 -0700 Subject: [PATCH 30/48] deleted: lib/config/io.sh new file: lib/config/iolocal.sh new file: lib/config/ioremote.sh new file: lib/config/iowirless.sh --- bin/config.sh | 2 +- lib/config/boardled.sh | 64 ----------------------------------------- lib/config/desktops.sh | 4 +-- lib/config/io.sh | 17 ----------- lib/config/iolocal.sh | 57 ++++++++++++++++++++++++++++++++++++ lib/config/ioremote.sh | 19 ++++++++++++ lib/config/iowirless.sh | 18 ++++++++++++ 7 files changed, 97 insertions(+), 84 deletions(-) delete mode 100644 lib/config/boardled.sh delete mode 100644 lib/config/io.sh create mode 100644 lib/config/iolocal.sh create mode 100644 lib/config/ioremote.sh create mode 100644 lib/config/iowirless.sh diff --git a/bin/config.sh b/bin/config.sh index b3eb4fe0c..1c7a2d5fe 100755 --- a/bin/config.sh +++ b/bin/config.sh @@ -72,7 +72,7 @@ done echo -e "Options:" echo -e " -h) Print this help." echo -e "" - echo -e " foo) Usage: ${filename%.*} foo $usage:: " + echo -e " foo) Usage: ${filename%.*} foo $usage " echo "" # Group options by prefix diff --git a/lib/config/boardled.sh b/lib/config/boardled.sh deleted file mode 100644 index c8eb58b64..000000000 --- a/lib/config/boardled.sh +++ /dev/null @@ -1,64 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# System boards led monitoring. See -# TBD - -# @description See a list of board led options. -# -# @example -# boardled::set_sysled -# #Output -# Led blinks to set $1 -# -# @exitcode 0 If successful. -# -# @stdout tbd. -boardled::see_sysled(){ - - # the avalible options - readarray triggers_led < <( cat /sys/class/leds/*/trigger ) - # see pass not argument the avalible options - [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; - # Set the systme Led blink to $1 valus - [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; - -} - -# @description Set board led options to none (off). -# -# @exitcode 0 If successful. -# -# @stdout tbd. -boardled::see_sysled_none(){ - - echo "none"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; - -} - -# @description Set board led options to monitor CPU. -# -# @exitcode 0 If successful. -# -# @stdout tbd. -boardled::see_sysled_cpu(){ - - echo "cpu"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; - -} - -# @description Set board led options to heartbeat pulse. -# -# @exitcode 0 If successful. -# -# @stdout tbd. -boardled::see_sysled_beat(){ - - echo "heartbeat"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; - -} diff --git a/lib/config/desktops.sh b/lib/config/desktops.sh index 8024962d4..9852e796d 100644 --- a/lib/config/desktops.sh +++ b/lib/config/desktops.sh @@ -10,7 +10,7 @@ # @description Display a list of avalible desktops to install. # # @example -# desk_setup::see_desktops +# desktops::see_desktops # echo $? # #Output # 0 @@ -18,7 +18,7 @@ # @exitcode 0 If successful. # # @stdout list of avalible desktops. -desk_setup::see_desktops(){ +desktops::see_desktops(){ apt-cache search armbian-$(grep VERSION_CODENAME /etc/os-release | cut -d"=" -f2)-desktop- | cut -d" " -f1 diff --git a/lib/config/io.sh b/lib/config/io.sh deleted file mode 100644 index c08127704..000000000 --- a/lib/config/io.sh +++ /dev/null @@ -1,17 +0,0 @@ - -# @description Enable or Disable Infrared Remote Control. -# -# @example -# io::set_ir_toggle enable -# io::set_ir_toggle disable -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -io::set_ir_toggle(){ - -[[ "$1" == "enable" ]] && sudo apt -y --no-install-recommends install lirc ; -[[ "$1" == "disabe" ]] && sudo apt -y remove lirc ; sudo apt -y -qq autoremove ; - -} \ No newline at end of file diff --git a/lib/config/iolocal.sh b/lib/config/iolocal.sh new file mode 100644 index 000000000..0fd1e263f --- /dev/null +++ b/lib/config/iolocal.sh @@ -0,0 +1,57 @@ + +# @description Enable or Disable Infrared Remote Control support. +# +# @example +# io::set_ir_toggle enable +# io::set_ir_toggle disable +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +iolocal::set_ir_toggle(){ + +[[ "$1" == "enable" ]] && sudo apt -y --no-install-recommends install lirc ; exit 0 ; +[[ "$1" == "disabe" ]] && sudo apt -y remove lirc ; sudo apt -y -qq autoremove ; exit 0 ; + +} + +# @description See a list of board led options. +# +# @example +# boardled::set_sysled +# #Output +# Led blinks to set $1 +# +# @exitcode 0 If successful. +# +# @stdout tbd. +boardled::see_sysled_opt(){ + + # the avalible options + readarray triggers_led < <( cat /sys/class/leds/*/trigger ) + # see pass not argument the avalible options + [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; + exit 0 +} + +# @description See a list of board led options. +# +# @example +# boardled::set_sysled +# #Output +# Led blinks to set $1 +# +# @exitcode 0 If successful. +# +# @stdout tbd. +boardled::set_sysled(){ + + # the avalible options + readarray triggers_led < <( cat /sys/class/leds/*/trigger ) + # see pass not argument the avalible options + [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; + # Set the systme Led blink to $1 valus + [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; + +} diff --git a/lib/config/ioremote.sh b/lib/config/ioremote.sh new file mode 100644 index 000000000..dac2c3de3 --- /dev/null +++ b/lib/config/ioremote.sh @@ -0,0 +1,19 @@ + +# @description Enable or Disable remote IO devices. (eth, urt, ssh, ... ?) +# +# @example +# io::set_toggle enable +# io::set_toggle disable +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +ioremote::set_toggle(){ + +[[ "$1" == "" ]] && echo "enable\ndisable"; +[[ "$1" == "enable" ]] && echo "ToDo; enable place holder" ; +[[ "$1" == "disabe" ]] && echo "ToDo; disable place holder" ; + +exit 0 +} \ No newline at end of file diff --git a/lib/config/iowirless.sh b/lib/config/iowirless.sh new file mode 100644 index 000000000..5858229a9 --- /dev/null +++ b/lib/config/iowirless.sh @@ -0,0 +1,18 @@ + +# @description Enable or Disable wireless IO devices. (wifi, bluetooth, ... lora?) +# +# @example +# io::set_toggle enable +# io::set_toggle disable +# echo $? +# #Output +# 0 +# +# @exitcode 0 If successful. +iowireless::set_toggle(){ + +[[ "$1" == "" ]] && echo "enable\ndisable"; +[[ "$1" == "enable" ]] && echo "ToDo; enable place holder" ; +[[ "$1" == "disabe" ]] && echo "ToDo; disable place holder" ; + +} \ No newline at end of file From 0d82d34d4db7b3a3bd2bdb67b1e3feea82fdd864 Mon Sep 17 00:00:00 2001 From: Joey Turner Date: Wed, 6 Dec 2023 12:34:25 -0700 Subject: [PATCH 31/48] Initial refactoring --- README.md | 167 +- bin/armbian-configng | 222 ++ bin/armbian-interface | 119 + bin/config.sh | 130 - bin/jampi-config | 224 -- lib/armbian-configng/documents.sh | 388 +++ lib/armbian-configng/functions.sh | 145 + lib/armbian-configng/network/readme.md | 3 + lib/armbian-configng/network/set_wifi.sh | 19 + lib/armbian-configng/system/hello_world.sh | 21 + lib/armbian-configng/system/readme.md | 2 + lib/armbian-configng/system/see_monitor.sh | 45 + lib/bash-utility-master/.editorconfig | 22 + lib/bash-utility-master/.gitignore | 3 + lib/bash-utility-master/.remarkrc | 8 + lib/bash-utility-master/CODE_OF_CONDUCT.md | 76 + lib/bash-utility-master/CONTRIBUTING.md | 129 + lib/bash-utility-master/LICENSE | 21 + lib/bash-utility-master/README.md | 3026 +++++++++++++++++ lib/bash-utility-master/bash_utility.sh | 20 + lib/bash-utility-master/bin/bashdoc.awk | 275 ++ .../bin/generate_readme.sh | 400 +++ .../image/bash-utility.png | Bin 0 -> 32396 bytes lib/bash-utility-master/image/logo.png | Bin 0 -> 23716 bytes .../src}/array.sh | 0 .../src}/check.sh | 0 .../src}/collection.sh | 0 .../src}/date.sh | 0 .../src}/debug.sh | 0 .../src}/file.sh | 0 .../src}/format.sh | 0 .../src}/interaction.sh | 0 .../src}/json.sh | 0 .../src}/misc.sh | 0 .../src}/os.sh | 0 .../src}/string.sh | 0 .../src}/terminal.sh | 0 .../src}/validation.sh | 0 .../src}/variable.sh | 0 lib/config/benchymark.sh | 57 - lib/config/cpu.sh | 199 -- lib/config/desktops.sh | 25 - lib/config/extradrives.sh | 80 - lib/config/iolocal.sh | 57 - lib/config/ioremote.sh | 19 - lib/config/iowirless.sh | 18 - share/armbian-configng/armbian-configng.svg | 5 + .../data/armbian-configng.csv | 4 + .../data/armbian-configng.json | 26 + share/armbian-configng/index.html | 90 + 50 files changed, 5134 insertions(+), 911 deletions(-) mode change 100644 => 100755 README.md create mode 100755 bin/armbian-configng create mode 100755 bin/armbian-interface delete mode 100755 bin/config.sh delete mode 100755 bin/jampi-config create mode 100644 lib/armbian-configng/documents.sh create mode 100644 lib/armbian-configng/functions.sh create mode 100644 lib/armbian-configng/network/readme.md create mode 100644 lib/armbian-configng/network/set_wifi.sh create mode 100644 lib/armbian-configng/system/hello_world.sh create mode 100644 lib/armbian-configng/system/readme.md create mode 100644 lib/armbian-configng/system/see_monitor.sh create mode 100644 lib/bash-utility-master/.editorconfig create mode 100644 lib/bash-utility-master/.gitignore create mode 100644 lib/bash-utility-master/.remarkrc create mode 100644 lib/bash-utility-master/CODE_OF_CONDUCT.md create mode 100644 lib/bash-utility-master/CONTRIBUTING.md create mode 100644 lib/bash-utility-master/LICENSE create mode 100644 lib/bash-utility-master/README.md create mode 100755 lib/bash-utility-master/bash_utility.sh create mode 100755 lib/bash-utility-master/bin/bashdoc.awk create mode 100755 lib/bash-utility-master/bin/generate_readme.sh create mode 100644 lib/bash-utility-master/image/bash-utility.png create mode 100644 lib/bash-utility-master/image/logo.png rename lib/{bash-utility => bash-utility-master/src}/array.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/check.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/collection.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/date.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/debug.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/file.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/format.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/interaction.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/json.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/misc.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/os.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/string.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/terminal.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/validation.sh (100%) mode change 100644 => 100755 rename lib/{bash-utility => bash-utility-master/src}/variable.sh (100%) mode change 100644 => 100755 delete mode 100644 lib/config/benchymark.sh delete mode 100644 lib/config/cpu.sh delete mode 100644 lib/config/desktops.sh delete mode 100644 lib/config/extradrives.sh delete mode 100644 lib/config/iolocal.sh delete mode 100644 lib/config/ioremote.sh delete mode 100644 lib/config/iowirless.sh create mode 100644 share/armbian-configng/armbian-configng.svg create mode 100755 share/armbian-configng/data/armbian-configng.csv create mode 100755 share/armbian-configng/data/armbian-configng.json create mode 100755 share/armbian-configng/index.html diff --git a/README.md b/README.md old mode 100644 new mode 100755 index bf66bb5c6..f7fb4dc29 --- a/README.md +++ b/README.md @@ -1,114 +1,77 @@ -# configng -This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility) -embedded in this project. This allows for functional programming in Bash. Error handling and validation are also included. -The idea is to provide an API in Bash that can be called from a Command line interface, Text User interface and others. -Why Bash? Well, because it's going to be in every distribution. Striped down distributions -may not include Python, C/C++, etc. build/runtime environments -## Quick start -* `sudo apt install git` -* `cd ~/` -* `git clone https://github.com/armbian/configng.git` -* `bash ~/configng/config.sh` - -#### If all goes well you should see list or avalible commands -``` -Usage: config [ -h | foo ] - -Options: - -h) Print this help. - - foo) Usage: config foo [ desk_setup::options ][ io::options ][ boardled::options ][ cpu::options ][ extra_drive::options ][ benchymark::options ]:: - - desk_setup::options - see_desktops Display a list of avalible desktops to install. +

+ Armbian logo +
+ Armbian ConfigNG +
+ CodeFactor +

- io::options - set_ir_toggle [ enable ][ disable ] Infrared Remote Control. - - boardled::options - see_sysled See a list of board led options. - see_sysled_none Set board led options to none (off). - see_sysled_cpu Set board led options to monitor CPU. - see_sysled_beat Set board led options to heartbeat pulse. +# User guide +## Quick start +Run the following commands: + + sudo apt install git + cd ~/ + git clone https://github.com/armbian/configng.git + cd configng + ./bin/armbian-configng --dev + +If all goes well you should see the Text-Based User Inerface (TUI) + +### To see a list of all functions and their descriptions, run the following command: +~~~ +bash ~/configng/bin/armbian-configng -h +~~~ +## Coding Style +follow the following coding style: +~~~ +# @description A short description of the function. +# +# @exitcode 0 If successful. +# +# @options A description if there are options. +function group::string() {s + echo "hello world" + return 0 +} +~~~ +## Codestyle can be used to auto generate + - [Markdown](share/armbian-configng/readme.md) + - [JSON](share/armbian-configng/data/armbian-configng.json) + - [CSV](share/armbian-configng/data/armbian-configng.csv) + - [HTML](share/armbian-configng/armbian-configng-table.html) + - [github.io](//tearran/github.io/armbian-configng/index.html) +## Functions list as of 2023-12-05 +## network +System and Security - cpu::options - see_policy Return policy as int based on original armbian-config logic. - see_freqs Return CPU frequencies as string delimited by space. - see_min_freq Return CPU minimum frequency as string. - see_max_freq Return CPU maximum frequency as string. - see_governor Return CPU governor as string. - see_governors Return CPU avalible governors as string delimited by space. - set_freq ** disabled ** Set min, max and CPU governor. +### set_wifi.sh - extra_drive::options - set_spi_vflash Set up a simulated MTD spi flash for testing. - rem_spi_vflash Remove tsting simulated MTD spi flash. + - **Group Name:** network + - **Action Name:** NMTUI + - **Options:** none. + - **Description:** Network Manager. - benchymark::options - see_monitor system boot-up performance statistics. - see_boot_times system boot-up performance statistics. +## system +Network Wired wireless Bluetooth access point -``` -#### Change the systems led to pulse a hearbeat -``` - bash ~/configng/bin/config.sh foo boardled::see_sysled_beat -``` -#### Change the systems led to off show a result in whiptail or dialog if installed -``` - bash ~/configng/bin/config.sh foo boardled::see_sysled_none | bash ~/configng/bin/jampi-config.sh -``` -#### See avalible settings sytem led options and current setting in [] -``` - bash ~/configng/bin/config.sh foo boardled::see_sysled -``` -#### See avalible armbian monitor options -``` - bash ~/configng/bin/config.sh foo benchymark:see_monitor -``` +### hello_world.sh + - **Group Name:** system + - **Action Name:** Hello + - **Options:** none + - **Description:** Hello System. -## Coding standards -[Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas, -but fundementally look at the code in lib: -``` -# @description Strip characters from the beginning of a string. -# -# @example -# echo "$(string::lstrip "Hello World!" "He")" -# #Output -# llo World! -# -# @arg $1 string The input string. -# @arg $2 string The characters you want to strip. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout Returns the modified string. -string::lstrip() { -[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 -printf '%s\n' "${1##$2}" -} -``` +### see_monitor.sh -Functions should follow ~~filename~~::func_name style. Then you can tell just from the name which -file the function is located in. Return codes should also follow a similar pattern: -* 0 Successful -* 1 Not found -* 2 Function missing arguments -* 3-255 all other errors + - **Group Name:** monitor + - **Action Name:** Bencharking + - **Options:** + - **Description:** Armbian Monitor and Bencharking. -Validate values: -``` -# Validate minimum frequency is <= maximum frequency -[ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 -``` -Return values should use stdout: -``` -# Return value -printf '%s\n' "$(cat $file)" -``` +# Inclueded projects +- [Bash Utility](https://labbots.github.io/bash-utility) +- [Armbian config](https://github.com/armbian/config.git) -Only use sudo when needed and never run as root! diff --git a/bin/armbian-configng b/bin/armbian-configng new file mode 100755 index 000000000..2a6c89774 --- /dev/null +++ b/bin/armbian-configng @@ -0,0 +1,222 @@ +#!/bin/bash + +# This script provides a command-line interface for managing Armbian configuration. +# It loads libraries of functions from the lib directory and displays them in a menu. +# The user can select a function to run, or use a text-based user interface (TUI) to navigate the menus. +# The script also provides a help option and a debug option for developers. +# The script requires sudo privileges to run some functions. +# The script uses whiptail or dialog for the TUI, depending on which is available. + +#set -x +#set -e + +# +# Enable Dynamic directory root use home ~/ , /bin , /usr/sbin etc.. +bin="$(dirname "${BASH_SOURCE[0]}")" +directory="$(cd "$bin/../" && pwd )" +file_name="$(basename "${BASH_SOURCE[0]}")" +filename="${file_name%.*}" +libpath=$(cd "$directory/lib/$filename/" && pwd) +#sharepath=$(cd "$directory/share/${filename%-dev}/" && pwd) + + +# +# Consept Distribution Compatibility checks +check_distro() { + + [[ -f "/usr/bin/${filename%%-*}-config" ]] && distro_config="${filename%%-*}" + [[ -f "/etc/${filename%%-*}-release" ]] && distro_release="${filename%%-*}" + # if both true then we are good to go + [[ -z "$distro_config" ]] || [[ -z "$distro_release" ]] && echo "W: Costum build, Tech support links are missing." + [[ -n "$distro_config" ]] && [[ -n "$distro_release" ]] && echo "I: This build seems to be community supported" | ./armbian-interface -o + [[ -f "/usr/sbin/${filename%%-*}-config" ]] && distro_config="${filename%%-*}" + [[ -f "/etc/${filename%%-*}-release" ]] && distro_release="${filename%%-*}" + +} + +[[ "$1" == "--dev" ]] && dev=1 && shift 1 + +# +# Check if the script is dev version. +suffix="${file_name##*-}" + +if [[ "$suffix" == dev ]]; then + dev=1 + check_distro #| armbian-interface -o +fi + +if [[ "$(id -u)" != "0" ]] && [[ "$dev" == "1" ]] ; then + +cat << EOF #| ./armbian-interface -o +I: Running in UX development mode +W: Admin functions will not work as expected + +EOF +elif [[ "$(id -u)" == "0" ]] && [[ "$dev" == "1" ]] ; then +cat << EOF | ./armbian-interface -o +I: Running in UX development mode +W: Document files may need Admin privleges to edit/remove + +EOF + +fi + +# +# Check if the script is being run as root +# UX Development mode bypasses root check, many functions will not work as expected + +if [[ "$(id -u)" != "0" ]] && [[ "$dev" != "1" ]]; then + echo -e "E: This tool requires root privileges. Try: \"sudo $filename\"" >&2 + exit 1 +fi + +declare -A dialogue + +# +# Check if whiptail or dialog is installed and set the variable 'dialogue' accordingly. +# todo add a fallback TUI and GUI +if command -v whiptail &> /dev/null; then + dialogue="whiptail" +elif command -v dialog &> /dev/null; then + dialogue="dialog" +else + echo "TUI not found" + echo "Warning: Using fallback TUI" + sleep 1 + clear && generate_read +fi + +source "$libpath/functions.sh" +source "$libpath/documents.sh" +for file in "$libpath"/*/*.sh; do + source "$file" +done + +# +# mapfile -t categories < <(ls -d "$libpath"/* ) +mapfile -t categories < <(find "$libpath"/* -type d) +declare -A functions + +for category in "${categories[@]}"; do + category_name="${category##*/}" + + category_file="$category/readme.md" + if [[ -f "$category_file" ]]; then + category_description=$(grep -oP "(?<=# @description ).*" "$category_file") + fi + + for file in "$category"/*.sh; do + description="" + while IFS= read -r line; do + if [[ $line =~ ^#\ @description\ (.*)$ ]]; then + description="${BASH_REMATCH[1]}" + elif [[ $line =~ ^function\ (.*::.*)\(\)\{$ ]]; then + # END: be15d9bcejpp + function_name="${BASH_REMATCH[1]}" + key="$category_name:${file##*/}:${function_name}" + functions["$key,function_name"]=$(echo "$function_name" | sed 's/.*:://') + functions["$key,group_name"]=$(echo "$function_name" | sed 's/::.*//') + functions["$key,description"]=$description + elif [[ $line =~ ^#\ @options\ (.*)$ ]]; then + functions["$key,options"]="${BASH_REMATCH[1]}" + fi + done < "$file" + functions["$key,category"]=$category_name + functions["$key,category_description"]=$category_description + done +done + + +# +# WIP: Check arguments for no flag options +# armbian-config --help +# Change to BASH: /usr/sbin/armbian-config main=System selection=BASH +handle_no_flag(){ +if [[ "$1" == *"="* ]]; then + IFS='=' read -r key value <<< "$1" + function_name=$(parse_action "$key" "$value") + # Call the function using variable indirection + ${function_name} +elif [[ "$1" == "help"* ]]; then + generate_list_cli +fi +} + +# +# Check arguments for long flag options +# Help message related to the functions the back end +handle_long_flag(){ + if [[ "$1" == "--help" ]]; then + generate_list_run + exit 0 ; + elif [[ "$1" == "--doc" ]]; then + generate_doc + exit 0 ; + fi +# WIP: + if [ "$1" == "--run" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-r") + group_name="$1" # Takes the first argument as the group name + shift 1 # Shifts the arguments again to exclude the group name + + function_name=$(parse_action "$group_name" "$1") + if [ $? -eq 0 ]; then + # Call the function using variable indirection + ${function_name} + fi + elif [ "$1" == "--help" ]; then + generate_list_run + exit + elif [ "$1" == "--test" ]; then + check_distro | armbian-interface -o && $1 > /dev/null + fi + +} +# +# Check arguments for short flag options +# THe interface help message +handle_short_flag(){ +if [ "$1" == "-h" ]; then + generate_help + exit 0 ; +# Generate a text-based user interface +elif [ "$1" == "-t" ] ; then + generate_read ; exit 0 ; +# Generate all doc files +elif [ "$1" == "-d" ] ; then + generate_doc ; exit 0 ; +elif [ "$1" == "-j" ] ; then + generate_json ; exit 0 ; +fi + +} + +case "$1" in + *"="*) + # Handle the case where $1 contains "=" + handle_no_flag "$@" + ;; + *"--"*) + # Handle the case where $1 starts with "--" + handle_long_flag "$@" + ;; + *"-"*) + # Handle the case where $1 starts with "-" + handle_short_flag "$1" + ;; + *) + handle_no_flag "$@" + # Handle the case where $1 does not match any of the above patterns + # You can add your code here + ;; +esac + +if [[ -z "$1" ]] ; then + while true; do + generate_tui ; + if [[ "$?" == "0" ]]; then + exit 0 + fi + done + +fi \ No newline at end of file diff --git a/bin/armbian-interface b/bin/armbian-interface new file mode 100755 index 000000000..ff095fdc0 --- /dev/null +++ b/bin/armbian-interface @@ -0,0 +1,119 @@ +#!/bin/bash + +# +# Copyright (c) 2023 Joseph C Turner +# All rights reserved. +# +# This script. +# demonstrates the compatibility of multiple interfaces for displaying menus or messages. +# It uses an array to set the options for all three menus (bash, whiptail, and dialog). +# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. +# If neither of these programs is available, it falls back to using bash. +# while both are installed falls back to whiptail to display the menu. +# The user can override the default program by passing an argument when running the script: +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +## DIRECTORY variable to the absolute path of the script's directory +directory="$(dirname "$(readlink -f "$0")")" +filename=$(basename "${BASH_SOURCE[0]}") + +## DIALOG variable to the absolute path of the script's directory +DIALOG="bash" +[[ -x "$(command -v dialog)" ]] && DIALOG="dialog" +[[ -x "$(command -v whiptail)" ]] && DIALOG="whiptail" + +show_help(){ + + echo -e "\nUsage: [command] | ${filename%.*} [ -h | -m | -o ]" + echo "Options:" + echo " -h, Print this help." + echo "" + echo " -o, Opens an OK message Box" + echo "" + echo " -m, Opens an Menu select Box." + echo "" + echo " -p, Opens Popup message box. " + echo "" + exit 1; + } + +show_message(){ + + # Read the input from the pipe continuously until there is no more input + input="" + while read -r line; do + input+="$line\n" + done + + # Display the "OK" message box with the input data + [[ $DIALOG != "bash" ]] && $DIALOG --title "Message Box" --msgbox "$input" 0 0 + [[ $DIALOG == "bash" ]] && echo -e "$input" + [[ $DIALOG == "bash" ]] && read -p "Press [Enter] to continue..." ; echo "" ; exit 1 + + } + +show_popup(){ + + + input="" + while read -r line; do + input+="$line\n" + done + + [[ $DIALOG != "bash" ]] && $DIALOG --title "Popup Box" --infobox "$input" 0 0 + [[ $DIALOG == "bash" ]] && echo -e "$input" + + } + +show_menu(){ + + + # Get the input and convert it into an array of options + inpu_raw=$(cat) + # Remove the lines befor -h + input=$(echo "$inpu_raw" | sed 's/-\([a-zA-Z]\)/\1/' | grep '^ [a-zA-Z] ' | grep -v '\[') + options=() + while read -r line; do + package=$(echo "$line" | awk '{print $1}') + description=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ *//') + options+=("$package" "$description") + done <<< "$input" + + # Display the menu and get the user's choice + [[ $DIALOG != "bash" ]] && choice=$($DIALOG --title "Menu" --menu "Choose an option:" 0 0 9 "${options[@]}" 3>&1 1>&2 2>&3) + + # Check if the user made a choice + if [ $? -eq 0 ]; then + echo "$choice" + else + echo "You cancelled." + fi + } + +[[ $1 == "-m" ]] && show_menu ; +[[ $1 == "-o" ]] && show_message ; +[[ $1 == "-h" ]] && show_help ; +[[ $1 == "-p" ]] && show_popup ; +[[ -z "$@" ]] && show_help ; diff --git a/bin/config.sh b/bin/config.sh deleted file mode 100755 index 1c7a2d5fe..000000000 --- a/bin/config.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# - -directory="$(dirname "$(readlink -f "$0")")" -filename=$(basename "${BASH_SOURCE[0]}") - -libpath="$directory/../lib" - -if [[ -d "$directory/../lib" ]]; then - libpath="$directory"/../lib -# installed option todo change when lib location determined -#elif [[ ! -d "$directory/../lib" && -d "/usr/lib/bash-utility/" && -d "/usr/lib/config/" ]]; then -# libpath="/usr/lib" -else - echo "Libraries not found" - exit 0 -fi - -# Source the files relative to the script location -for file in "$libpath"/bash-utility/*; do - source "$file" -done -for file in "$libpath"/config/*; do - source "$file" -done - -functionarray=() -funnamearray=() -catagoryarray=() -descriptionarray=() - -for file in "$libpath"/config/*.sh; do - mapfile -t temp_functionarray < <(grep -oP '^\w+::\w+' "$file") - functionarray+=("${temp_functionarray[@]}") - - mapfile -t temp_funnamearray < <(grep -oP '^\w+::\w+' "$file" | sed 's/.*:://') - funnamearray+=("${temp_funnamearray[@]}") - - mapfile -t temp_catagoryarray < <(grep -oP '^\w+::\w+' "$file" | sed 's/::.*//') - catagoryarray+=("${temp_catagoryarray[@]}") - - mapfile -t temp_descriptionarray < <(grep -oP '^# @description.*' "$file" | sed 's/^# @description //') - descriptionarray+=("${temp_descriptionarray[@]}") -done - - see_help_dev(){ - # Extract unique prefixes - declare -A prefixes - for i in "${!functionarray[@]}"; do - prefix="${functionarray[i]%%::*}" - prefixes["$prefix"]=1 - done - - # Construct usage line - usage="" - for prefix in "${!prefixes[@]}"; do - usage+="[ $prefix::options ]" - done - - -# echo "$usage" - echo "Usage: ${filename%.*} [ -h | foo ]" - echo "" - echo -e "Options:" - echo -e " -h) Print this help." - echo -e "" - echo -e " foo) Usage: ${filename%.*} foo $usage " - echo "" - - # Group options by prefix - declare -A groups - for i in "${!functionarray[@]}"; do - prefix="${functionarray[i]%%::*}" - suffix="${functionarray[i]#*::}" - groups["$prefix"]+=$'\t\t'"$suffix\t${descriptionarray[i]}"$'\n' - done - - # Print grouped options - for group in "${!groups[@]}"; do - echo -e " $group::options" - echo -e "${groups[$group]}" - done -} - - -# TEST 7 -# check for -h -dev @ $1 -# if -dev check @ $1 remove and shift $1 to check for x -check_opts() { - if [ "$1" == "foo" ]; then - shift # Shifts the arguments to the left, excluding the first argument ("-dev") - function_name="$1" # Assigns the next argument as the function name - shift # Shifts the arguments again to exclude the function name - - found=false - - for ((i=0; i<${#functionarray[@]}; i++)); do - if [ "$function_name" == "${functionarray[i]}" ]; then - found=true - ${functionarray[i]} "$@" - break - fi - done - - if [ "$found" == false ]; then - echo "Invalid function name" - see_help_dev - exit 1 - - fi - - elif [[ "$1" == "foo" && "$2" == "cpu::set_freq" ]]; then - # Disabled till understood. - echo "cpu::set_freq policy min_freq max_freq performance" - echo "Disabled durring current testing" - - else - see_help_dev - fi -} - -check_opts "$@" diff --git a/bin/jampi-config b/bin/jampi-config deleted file mode 100755 index f9f64e34b..000000000 --- a/bin/jampi-config +++ /dev/null @@ -1,224 +0,0 @@ -#!/bin/bash - -# -# Copyright (c) 2023 Joseph C Turner -# All rights reserved. -# -# This script. -# demonstrates the compatibility of multiple interfaces for displaying menus or messages. -# It uses an array to set the options for all three menus (bash, whiptail, and dialog). -# The script checks if whiptail or dialog are available on the system and uses them to display the menu in a more user-friendly way. -# If neither of these programs is available, it falls back to using bash. -# while both are installed falls back to whiptail to display the menu. -# The user can override the default program by passing an argument when running the script: -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# - -# Comeback to this later -# -##DIRECTORY variable to the absolute path of the script's directory -##directory=$(cd "$(dirname "$0")" && pwd) -#directory="$(dirname "$(readlink -f "$0")")" -#filename=$(basename "${BASH_SOURCE[0]}") -#selfpath="$directory"/"$filename" -##libpath="${directory}"/"its-lib" #Include these scripts Library -#libpath="$selfpath" -# -# -#clear -## Check for the availability of whiptail and dialog command-line programs -## Set the default program to use for displaying messages -#( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="bash" -#( command -v whiptail >/dev/null && ! command -v dialog >/dev/null ) && default="whiptail" -#( ! command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="dialog" -# -## if both whiptail and dialog change to prefered -#( command -v whiptail >/dev/null && command -v dialog >/dev/null ) && default="whiptail" -# -# -#[[ "$1" == -b ]] && default="bash" -#[[ "$1" == -w ]] && default="whiptail" -#[[ "$1" == -n ]] && default="dialog" -# -#[[ "$1" == -m ]] && { -#export NEWT_COLORS=" -#root=blue,black -#border=green,black -#title=green,black -#roottext=red,black -#window=red,black -#textbox=white,black -#button=black,green -#compactbutton=white,black -#listbox=white,black -#actlistbox=black,white -#actsellistbox=black,green -#checkbox=green,black -#actcheckbox=black,green -#" -#} -# -## Check the cpu architecture. for later handeling if nessery -#architecture=$(dpkg --print-architecture) -#[[ "$architecture" == "armf" ]] && true ; -# -##setup menu arrays -#readarray -t functionarray < <( grep -A1 '^##.*@.*:*' "$libpath" | grep -oP '^\w+\(\)' | sed 's/()//' ) -#readarray -t descriptions < <( grep -e '^##.*@.*' "$libpath" | sed "s|^## *@||g;s| ## *@.*||g;s|.*: *||g" ) -#readarray -t descriptionarray < <( for i in "${!descriptions[@]}" ; do printf "%s\n %s\n" "$i" "${descriptions[i]}" ; done ) -# -### System@Settings:Advanced Settings (armbian-config) -#cmd_advance() -#{ -# sudo armbian-config -#} -# -### System@CPU info:Example from Bash Utility (cpu_test.sh) -#cmd_cpu_info() -#{ -# [[ ! -f /usr/sbin/cpu_test_library.sh ]] && cmd_cpu_ls ; -# [[ -f /usr/sbin/cpu_test_library.sh ]] && shell_command=$(sudo /usr/sbin/cpu_test_library.sh ) -# message_box=$( printf '%s ' "${shell_command[@]}" ) ; -# see_message -#} -# -# -### System@CPU info:An example function (lscpu) -#cmd_cpu_ls() -#{ -# -# shell_command="$(lscpu)" ; -# message_box=$( printf '%s ' "${shell_command[@]}" ) ; -# see_message -#} -# -### System@Bootup Time:An example function (systemd-analyze time) -#cmd_boot_time() -#{ -# -# shell_command="$(systemd-analyze time)" ; -# message_box=$( printf '%s ' "${shell_command[@]}" ) ; -# see_message -#} -# -### System@Login Info :An example function (Login Info) -#cmd_lslogins() -#{ -# -# shell_command="$(lslogins)" ; -# message_box=$( printf '%s ' "${shell_command[@]}" ) ; -# see_message -#} -# -# -## Function to display a message using whiptail, dialog or printf depending on what is available on the system -#see_message() -#{ -# # Use if neither whiptail nor dialog are available on the system -# if [[ "$default" == "bash" ]] || ( ! command -v whiptail >/dev/null && ! command -v dialog >/dev/null ); then -# # Use printf to display the message -# printf '%s ' "${shell_command[@]}" -# -# # Use as default if whiptail is available -# elif [[ "$default" == "whiptail" ]] && ( command -v whiptail >/dev/null ); then -# # Use whiptail to display the message -# whiptail --backtitle "newt whitail: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 --clear --scrolltext -# -# # Use if dialog is available on the system but not whiptail -# elif [[ "$default" == "dialog" ]] && ( command -v dialog >/dev/null ); then -# # Use dialog to display the message -# dialog --backtitle "ncurses dialog: $architecture" --title "description" --msgbox "${message_box[@]}" 0 0 ; clear -# fi -#} -# -#see_menu() -#{ -# -# if [[ "$default" == "bash" ]]; then -# PS3="Enter a number: " -# select i in "${descriptions[@]}" ; do ${functionarray[$REPLY - 1]} ; break ; done -# -# elif [[ "$default" == "whiptail" ]]; then -# OPTION=$(whiptail --backtitle "newt whiptail: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) -# [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" -# -# elif [[ "$default" == "dialog" ]]; then -# OPTION=$(dialog --backtitle "ncurses dialog: $architecture" --title "Config" --menu "Choose your option" 20 80 7 "${descriptionarray[@]}" 3>&1 1>&2 2>&3) -# [[ -n $OPTION ]] && clear && "${functionarray[$OPTION]}" -# fi -#} -# -#see_help(){ -# -# echo "" -# echo "Usage: ${filename%.*} [ -h | -b | -n | -w | -d | -dev ]" -# echo -e "Options:" -# echo -e " -h Print this help." -# echo -e " -b GNU bash " -# echo -e " -n NCURSES dialog " -# echo -e " -w NEWT whiptail - default colors " -# echo -e " -m dark mode whiptail " -# echo -e " -dev Options:" -# for i in "${!functionarray[@]}"; do -# printf '\t\t%s\t%s \n' "${functionarray[i]}" "${descriptions[i]}" -# done -# -# } -# -#main() -#{ -# if [[ "$1" == --dev ]] ; then -# default="bash" -# local found=false -# for i in "${!functionarray[@]}"; do -# if [ "$2" == "${functionarray[i]}" ]; then -# "${functionarray[i]}" -# found=true -# break -# fi -# done -# if ! $found; then -# see_help -# exit 0 -# fi -# elif [[ "$1" == -h ]] ; then -# see_help -# else -# see_menu -# fi -#} -# -#main "$@" - -if command -v whiptail > /dev/null; then - DIALOG=whiptail -elif command -v dialog > /dev/null; then - DIALOG=dialog -else - echo "Error: Neither whiptail nor dialog is installed." - exit 1 -fi - -input=$(cat) -$DIALOG --title "Config Options" --msgbox "$input" 0 0 diff --git a/lib/armbian-configng/documents.sh b/lib/armbian-configng/documents.sh new file mode 100644 index 000000000..8a319822c --- /dev/null +++ b/lib/armbian-configng/documents.sh @@ -0,0 +1,388 @@ +# This function is used to generate a simple JSON file containing all functions and their descriptions. +# pthon is more suited to complex arrays this should be handeled during build time +generate_json() { + json_objects=() + for key in "${!functions[@]}"; do + if [[ $key == *",function_name"* ]]; then + function_key="${key%,function_name}" + function_name="${functions[$key]}" + group_name="${functions["$function_key,group_name"]}" + description="${functions["$function_key,description"]}" + options="${functions["$function_key,options"]}" + category="${functions["$function_key,category"]}" + category_description="${functions["$function_key,category_description"]}" + json_objects+=("{ \"Function Name\": \"$function_name\", \"Group Name\": \"$group_name\", \"Description\": \"$description\", \"Options\": \"$options\", \"Category\": \"$category\", \"Category Description\": \"$category_description\" }") + fi + done + IFS=',' + echo "[${json_objects[*]}]" | jq +} + +# This function is used to generate a armbian CPU logo +generate_svg(){ + +cat << EOF + + + + + +EOF +} + +# This function is used to generate a CSV file containing all functions and their descriptions. +generate_csv() { + echo "Function Name,Group Name,Description,Options,Category,Category Description" + for key in "${!functions[@]}"; do + if [[ $key == *",function_name"* ]]; then + function_key="${key%,function_name}" + function_name="${functions[$key]}" + group_name="${functions["$function_key,group_name"]}" + description="${functions["$function_key,description"]}" + options="${functions["$function_key,options"]}" + category="${functions["$function_key,category"]}" + category_description="${functions["$function_key,category_description"]}" + echo "$function_name,$group_name,$description,$options,$category,$category_description" + fi + done +} + +generate_csv_test() { + for category in "${categories[@]}"; do + echo "Function Name,Group Name,Description,Options,Category,Category Description" > "$category.csv" + for key in "${!functions[@]}"; do + if [[ $key == *",function_name"* ]]; then + function_key="${key%,function_name}" + function_name="${functions[$key]}" + group_name="${functions["$function_key,group_name"]}" + description="${functions["$function_key,description"]}" + options="${functions["$function_key,options"]}" + category="${functions["$function_key,category"]}" + category_description="${functions["$function_key,category_description"]}" + if [[ $category == "$category" ]]; then + echo "$function_name,$group_name,$description,$options,$category,$category_description" >> "$category.csv" + fi + fi + done + done +} + +# This function is used to generate a Single page app. +generate_html5() { + +html5_content=' + + + + + Armbian '$(echo "$filename")' + + + +
+

'$(echo "$filename")'

+
+ +
+
+ +
+
+ + + + + +' + +echo "$html5_content" ; + +} + +# This function is used to generate tabe html file +# used to check proper array generation and output. +generate_html() { + html_content=' + + + + + + + + + + + + + + + + + ' + for key in "${!functions[@]}"; do + if [[ $key == *",function_name"* ]]; then + function_key="${key%,function_name}" + function_name="${functions[$key]}" + group_name="${functions["$function_key,group_name"]}" + description="${functions["$function_key,description"]}" + options="${functions["$function_key,options"]}" + category="${functions["$function_key,category"]}" + category_description="${functions["$function_key,category_description"]}" + html_content+="" + fi + done + html_content+=' + +
Function NameGroup NameDescriptionOptionsCategoryCategory Description
$function_name$group_name$description$options$category$category_description
+ + + ' + + echo "$html_content" +} + +# This function is used to generate the main readme.md file +generate_markdown() { +cat << EOF + +

+ Armbian logo +
+ Armbian ConfigNG +
+ CodeFactor +

+ +# User guide +## Quick start +Run the following commands: + + sudo apt install git + cd ~/ + git clone https://github.com/armbian/configng.git + cd configng + ./bin/${file_name%.*} --dev + +If all goes well you should see the Text-Based User Inerface (TUI) + +### To see a list of all functions and their descriptions, run the following command: +~~~ +bash ~/configng/bin/armbian-configng -h +~~~ +## Coding Style +follow the following coding style: +~~~ +# @description A short description of the function. +# +# @exitcode 0 If successful. +# +# @options A description if there are options. +function group::string() {s + echo "hello world" + return 0 +} +~~~ +## Codestyle can be used to auto generate + - [Markdown](share/${file_name%.*}/readme.md) + - [JSON](share/${file_name%.*}/data/${file_name%.*}.json) + - [CSV](share/${file_name%.*}/data/${file_name%.*}.csv) + - [HTML](share/${file_name%.*}/${file_name%.*}-table.html) + - [github.io](//tearran/github.io/${file_name%.*}/index.html) +## Functions list as of $(date +%Y-%m-%d) +EOF + + for category in "${categories[@]}"; do + echo "## ${category##*/}" + echo "${functions["$key,category_description"]}" + echo + + for file in "$category"/*.sh; do + echo "### ${file##*/}" + echo + + mapfile -t functions_in_file < <(grep -oP '(?<=function\s)\w+::\w+' "$file") + + for function in "${functions_in_file[@]}"; do + key="${category##*/}:${file##*/}:${function}" + echo " - **Group Name:** ${functions["$key,group_name"]}" + echo " - **Action Name:** ${functions["$key,function_name"]}" + echo " - **Options:** ${functions["$key,options"]}" + echo " - **Description:** ${functions["$key,description"]}" + echo + done + done + done +cat << EOF + +# Inclueded projects +- [Bash Utility](https://labbots.github.io/bash-utility) +- [Armbian config](https://github.com/armbian/config.git) + +EOF +} + + +# This function is used to generate a extention to help meassage of all functions and their descriptions. +generate_list_run() { + echo "Usage: ${filename%.*} [--run] [option] [action]" + # Loop through each category + for category in "${categories[@]}"; do + # Initialize an empty array to store the group names that have been printed + declare -A printed_groups + + # Loop through each file in the category + for file in "$category"/*.sh; do + + # Extract functions from the file + mapfile -t functions_in_file < <(grep -oP '(?<=function\s)\w+::\w+' "$file") + + # Loop through each function in the file + for function in "${functions_in_file[@]}"; do + key="${category##*/}:${file##*/}:${function}" + group_name=${functions["$key,group_name"]} + + # If the group name has not been printed yet, print it and add it to the array + declare -A printed_groups + if [[ -z ${printed_groups["$group_name"]} ]]; then + echo " $group_name, [action]" + printed_groups["$group_name"]=1 + fi + + echo " ${functions["$key,function_name"]} - ${functions["$key,description"]}" + echo + done + done + done + +} + +# This function is used to generate a no flag options help message +generate_list_cli() { + + echo "Usage: ${filename%.*} [group]=[function]" + # Loop through each category + for category in "${categories[@]}"; do + # Initialize an empty array to store the group names that have been printed + declare -A printed_groups + + # Loop through each file in the category + for file in "$category"/*.sh; do + + # Extract functions from the file + mapfile -t functions_in_file < <(grep -oP '(?<=function\s)\w+::\w+' "$file") + + # Loop through each function in the file + for function in "${functions_in_file[@]}"; do + key="${category##*/}:${file##*/}:${function}" + group_name=${functions["$key,group_name"]} + printf "\t%-20s - \t %s \n" "$group_name=${functions["$key,function_name"]}" "${functions["$key,description"]}" + done + done + done +} + + +# This function is used to generate a help message. +generate_help(){ +cat << EOF +Usage: ${filename%.*} [flag][option] + flag options: + -h, Print this help. + -t, Show a TUI fallback read. + --help, Prints Help message of long flag interactive options (WIP)." + help, View advanced no-interface options (CURRENT FOCUS)." +EOF +} + +# THis function is used to make documents +generate_and_print() { + local generate_func=$1 + local filename=$2 + local file_extension=$3 + local output_message=$4 + + "$generate_func" > "$filename.$file_extension" + chmod 755 "$filename.$file_extension" + echo "$output_message - generated $filename.$file_extension" +} + +generate_doc() { + dir="$(dirname "$(dirname "$(realpath "$0")")")/share/${filename%-dev}" + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir/data/" + fi + cd "$dir" || exit + generate_svg > "$filename.svg" + generate_and_print generate_markdown "../../readme" md "readme.md" + generate_and_print generate_html "../../index" html "index.html" + generate_and_print generate_html5 "index" html "HTML5" + generate_and_print generate_json "data/$filename" json "JSON" + generate_and_print generate_csv "data/${filename%-dev}" csv "CSV" + if [[ "$EUID" -eq 0 ]]; then + chown -R "$SUDO_USER":"$SUDO_USER" "$(dirname "$dir")" + cd ../../ + chown "$SUDO_USER":"$SUDO_USER" readme.md + fi + return 0 +} \ No newline at end of file diff --git a/lib/armbian-configng/functions.sh b/lib/armbian-configng/functions.sh new file mode 100644 index 000000000..3d598c61b --- /dev/null +++ b/lib/armbian-configng/functions.sh @@ -0,0 +1,145 @@ + + +# This function is used to generate a text-based user interface (TUI) for navigating the menus. +generate_tui() { + local options=() + local i=0 + declare -A categories_array + for category in "${categories[@]}"; do + local category_name="${category##*/}" + local category_description="" + local category_file="$category/readme.md" + + if [[ -f "$category_file" ]]; then + category_description=$(grep -oP "(?<=# @description ).*" "$category_file") + fi + + categories_array["$i"]="$category_name" + description_array["$i"]="$category_description" + options+=("$i" "$(printf '%-7s - %-8s' "${categories_array[$i]}" "${description_array[$i]}")") + #options+=("$i" "${categories_array[$i]} - ${description_array[$i]}") + ((++i)) + done + options+=("$i" "$(printf '%-7s - %-8s' "Legacy" "Run Legacy configuration")") + + #options+=("$i" "Legacy - Run Legacy configuration") + ((++i)) + options+=("$i" "$(printf '%-7s - %-8s' "Help" "Documentation, support, sources")") + #options+=("$i" "Help - Documentation, support, sources" ) + ((++i)) + + local choice + + choice=$($dialogue --menu "Select a category:" 0 0 9 "${options[@]}" 3>&1 1>&2 2>&3) + + if [[ -n $choice ]]; then + + if ((choice == "$i - 1")); then + generate_help | armbian-interface -o + exit ; + elif ((choice == "$i - 2")); then + armbian-config + exit ; + else + generate_sub_tui "${categories_array[$choice]}" + fi + fi +} + +# This function is used to generate a text-based user interface (TUI) for navigating the menus. +generate_sub_tui() { + local category="$1" + local options=() + local i=0 + declare -A functions_array + for file in "$libpath/$category"/*.sh; do + mapfile -t functions_in_file < <(grep -oP '(?<=function\s)\w+::\w+' "$file") + for function in "${functions_in_file[@]}"; do + key="${category##*/}:${file##*/}:${function}" + functions_array["$i"]="$function" + options+=("$i" "${functions["$key,function_name"]} - ${functions["$key,description"]}") + ((++i)) + done + done + + local choice + + choice=$($dialogue --menu "Select a function:" 0 0 9 "${options[@]}" 3>&1 1>&2 2>&3) + + if [[ -n $choice ]]; then + generate_action "${functions_array[$choice]}" + fi + +} + +# This function is used to generate a whiptail/dialog text-based user interface (TUI) for navigating the menus. +generate_action() { + local function_name="$1" + ${function_name} +} + +# This function is used to generate a bash text-based user interface (TUI) for navigating the menus. +generate_read() { + echo + echo "Please select an action:" + echo + # Initialize an empty array to store the function keys + declare -a function_keys + + # Loop through each key in the functions array + local i=1 + local current_category="" + for key in "${!functions[@]}"; do + if [[ $key == *",function_name" ]]; then + # Add the key to the function_keys array + function_keys[i]="${key%,function_name}" + + # Check if the category has changed and display it if so + local category="${functions["${function_keys[i]},category"]}" # < editor" + if [[ "$category" != "$current_category" ]]; then + echo "Category: $category" + current_category="$category" + fi + + # Display the function and its description as an option in the menu + echo " $i. ${functions["${function_keys[i]},group_name"]} ${functions[$key]} - ${functions["${function_keys[i]},description"]}" #" < for my editor + ((i++)) + fi + done + + echo + echo "$i. Show help" + ((i++)) + echo "$i. Exit" + + read -p "Enter your choice: " choice + + if ((choice == i-1)); then + generate_help + elif ((choice == i)); then + exit 0 + elif ((choice >= 1 && choice <= ${#function_keys[@]})); then + # Call the selected function using variable indirection + eval "${functions["${function_keys[choice]},group_name"]}::${functions["${function_keys[choice]},function_name"]}" #" < for my editor + else + echo "Invalid choice" + fi +} + +# This function is used to parse the action name and return the full function name. +parse_action() { + local group=$1 + local action=$2 + + # Construct the full function name + local function_name="${group}::${action}" + + # Check if the function exists + if declare -f "$function_name" > /dev/null; then + # Return the function name + echo "$function_name" + else + echo "Error: Unknown action '$action' for group '$group'" + return 1 + fi +} \ No newline at end of file diff --git a/lib/armbian-configng/network/readme.md b/lib/armbian-configng/network/readme.md new file mode 100644 index 000000000..646eff7e0 --- /dev/null +++ b/lib/armbian-configng/network/readme.md @@ -0,0 +1,3 @@ + +# @description Network Wired wireless Bluetooth access point + diff --git a/lib/armbian-configng/network/set_wifi.sh b/lib/armbian-configng/network/set_wifi.sh new file mode 100644 index 000000000..337a7d899 --- /dev/null +++ b/lib/armbian-configng/network/set_wifi.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. + + + +# @description Network Manager. +# +# @exitcode 0 If successful. +# +# @options none +function network::NMTUI(){ + nmtui connect + return 0 +} diff --git a/lib/armbian-configng/system/hello_world.sh b/lib/armbian-configng/system/hello_world.sh new file mode 100644 index 000000000..c30f21520 --- /dev/null +++ b/lib/armbian-configng/system/hello_world.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. + + +# @description Hello System. +# +# @exitcode 0 If successful. +# +# @options none. +# +function system::Hello(){ + + echo "Hello Armbian" + return 0 +} + diff --git a/lib/armbian-configng/system/readme.md b/lib/armbian-configng/system/readme.md new file mode 100644 index 000000000..7adee2ea4 --- /dev/null +++ b/lib/armbian-configng/system/readme.md @@ -0,0 +1,2 @@ + +# @description System and Security diff --git a/lib/armbian-configng/system/see_monitor.sh b/lib/armbian-configng/system/see_monitor.sh new file mode 100644 index 000000000..da1ef94d7 --- /dev/null +++ b/lib/armbian-configng/system/see_monitor.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. + + +# @description Armbian Monitor and Bencharking. +# +# @exitcode 0 If successful. +# +# @options none +function monitor::Bencharking(){ + see_menu #| armbian-interface -o + return 0 ; +} + +see_menu(){ + # Define the script + script="$(which armbianmonitor)" + + # Run the script with the -h option and save the output to a variable + help_message=$("$script" -h ) || exit 2 + + # Reformat the help message into an array line by line + readarray -t script_launcher < <(echo "$help_message" | sed 's/-\([a-zA-Z]\)/\1/' | grep '^ [a-zA-Z] ' | grep -v '\[') + + # Loop through each line in the array and create a menu string + menu_string="" + for line in "${script_launcher[@]}"; do + # Append the formatted line to the menu string + if [[ "$line" != " d "* ]] && [[ "$line" != " c "* ]]; then + menu_string+="$line\n" + fi + done + + # Use the get_help_msg function and pipe its output into configng-interface -m + selected_option=$(echo -e "$menu_string" | armbian-interface -m) + + # Run the armbian-monitor script with the selected option + [[ -n "$selected_option" ]] && "$script" -"$selected_option"; + } + diff --git a/lib/bash-utility-master/.editorconfig b/lib/bash-utility-master/.editorconfig new file mode 100644 index 000000000..f9b075876 --- /dev/null +++ b/lib/bash-utility-master/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# for shfmt +[*.sh] +indent_style = space +indent_size = 4 +shell_variant = bash +switch_case_indent = true +space_redirects = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/lib/bash-utility-master/.gitignore b/lib/bash-utility-master/.gitignore new file mode 100644 index 000000000..7c7bf7091 --- /dev/null +++ b/lib/bash-utility-master/.gitignore @@ -0,0 +1,3 @@ +/tmp +gh-pages +hugo-docs diff --git a/lib/bash-utility-master/.remarkrc b/lib/bash-utility-master/.remarkrc new file mode 100644 index 000000000..a3ff1e10d --- /dev/null +++ b/lib/bash-utility-master/.remarkrc @@ -0,0 +1,8 @@ +{ + "plugins": [ + "remark-preset-lint-markdown-style-guide", + ["remark-lint-list-item-spacing", false], + ["remark-lint-maximum-line-length", false], + ["remark-lint-no-duplicate-headings", false] + ] +} diff --git a/lib/bash-utility-master/CODE_OF_CONDUCT.md b/lib/bash-utility-master/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..461c926b9 --- /dev/null +++ b/lib/bash-utility-master/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at + +For answers to common questions about this code of conduct, see + + +[homepage]: https://www.contributor-covenant.org diff --git a/lib/bash-utility-master/CONTRIBUTING.md b/lib/bash-utility-master/CONTRIBUTING.md new file mode 100644 index 000000000..aa401df88 --- /dev/null +++ b/lib/bash-utility-master/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing to Bash-Utility + +:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: +The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Table of Contents +- [Code Contributions](#code-contributions) +- [Code Guidelines](#code-guidelines) + - [Styleguide](#styleguide) + - [Bashdoc guideline](#bashdoc-guideline) +- [Documentation](#documentation) +- [Commit Guidelines](#commit-guidelines) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Contact](#contact) + +## Code Contributions + +Great, the more, the merrier. + +Sane code contributions are always welcome, whether to the code or documentation. + +Before making a pull request, make sure to follow below guidelines: + +### Code Guidelines + +#### Styleguide + +- Variable names must be meaningful and self-documenting. +- Long variable names must be structured by underscores to improve legibility. +- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE) +- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data) +- Variable names can be alphanumeric with underscores. No special characters in variable names. +- Variables name must not start with number. +- Variables within function must be declared. So the scope of variable is restricted to the function. +- Avoid accessing global variables within functions. +- Function names must be all lower case with underscores to seperate words (snake_case). +- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`) +- Try using bash builtins and string substitution as much as possible. +- Use printf everywhere instead of echo. +- Before adding a new logic, be sure to check the existing code. +- Make sure to add the function in appropriate section based on its operation. +- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command: + + ```shell + shfmt upload.sh + ``` + + The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags. + You can also install shfmt for various editors, refer their repo for information. + Note: This is strictly necessary to maintain consistency, do not skip. + +- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason. + +#### Bashdoc guideline + +The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is +properly generated by the generator. + +Follow the below bashdoc template to add function introductory comment. + +```bash +# @description Multiline description goes here and +# there +# +# @example +# sample::function a b c +# echo 123 +# +# @arg $1 string Some arg. +# @arg $2 any Rest of arguments. +# +# @noargs +# +# @exitcode 0 If successfull. +# @exitcode >0 On failure +# @exitcode 5 On some error. +# +# @stdout Path to something. +# +# @see sample::other_function(() +sample::function() { +} +``` + +- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used. +- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment. +- Make sure to document the exitcode emitted by the function. +- If the function is similar to other function add a reference to function using @see tag. + +### Documentation + +- Refrain from making unnecessary newlines or whitespace. +- Use pure markdown as much as possible, html is accepted but shouldn't be a priority. +- The markdown must pass RemarkLint checks. +- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc. + + ```bash + ./bin/generate_readme.sh -f README.md -s src/ + ``` + +### Commit Guidelines + +It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted. + +It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message. + +The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation. + +Before committing check for unnecessary whitespace with `git diff --check`. + +### Pull Request Guidelines + +The following guidelines will increase the likelihood that your pull request will get accepted: + +- Follow the commit and code guidelines. +- Keep the patches on topic and focused. +- Try to avoid unnecessary formatting and clean-up where reasonable. + +A pull request should contain the following: + +- At least one commit (all of which should follow the Commit Guidelines). +- Title that summarises the issue/feature. +- Description that briefly summarises the changes. + +After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread. + +## Contact + +For further inquiries, you can contact the developer by opening an issue on the repository. diff --git a/lib/bash-utility-master/LICENSE b/lib/bash-utility-master/LICENSE new file mode 100644 index 000000000..99dd0836b --- /dev/null +++ b/lib/bash-utility-master/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 labbots + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/bash-utility-master/README.md b/lib/bash-utility-master/README.md new file mode 100644 index 000000000..9bc1f9484 --- /dev/null +++ b/lib/bash-utility-master/README.md @@ -0,0 +1,3026 @@ +

Bash Utility

+ +

+Stars +License + +

+

+Gh-pages Status +Website +

+

+Total number of Library functions +

+

+ +

+Bash library which provides utility functions and helpers for functional programming in Bash. + +Detailed documentation is available at + + + +## Table of Contents + +- [Installation](#installation) + - [Method 1 - Git Submodules](#method-1---git-submodules) + - [Method 2 - Git Clone](#method-2---git-clone) + - [Method 3 - Direct Download](#method-3---direct-download) +- [Usage](#usage) +- [Array](#array) + - [array::contains()](#arraycontains) + - [array::dedupe()](#arraydedupe) + - [array::is_empty()](#arrayis_empty) + - [array::join()](#arrayjoin) + - [array::reverse()](#arrayreverse) + - [array::random_element()](#arrayrandom_element) + - [array::sort()](#arraysort) + - [array::rsort()](#arrayrsort) + - [array::bsort()](#arraybsort) + - [array::merge()](#arraymerge) +- [Check](#check) + - [check::command_exists()](#checkcommand_exists) + - [check::is_sudo()](#checkis_sudo) +- [Collection](#collection) + - [collection::each()](#collectioneach) + - [collection::every()](#collectionevery) + - [collection::filter()](#collectionfilter) + - [collection::find()](#collectionfind) + - [collection::invoke()](#collectioninvoke) + - [collection::map()](#collectionmap) + - [collection::reject()](#collectionreject) + - [collection::some()](#collectionsome) +- [Date](#date) + - [date::now()](#datenow) + - [date::epoc()](#dateepoc) + - [date::add_days_from()](#dateadd_days_from) + - [date::add_months_from()](#dateadd_months_from) + - [date::add_years_from()](#dateadd_years_from) + - [date::add_weeks_from()](#dateadd_weeks_from) + - [date::add_hours_from()](#dateadd_hours_from) + - [date::add_minutes_from()](#dateadd_minutes_from) + - [date::add_seconds_from()](#dateadd_seconds_from) + - [date::add_days()](#dateadd_days) + - [date::add_months()](#dateadd_months) + - [date::add_years()](#dateadd_years) + - [date::add_weeks()](#dateadd_weeks) + - [date::add_hours()](#dateadd_hours) + - [date::add_minutes()](#dateadd_minutes) + - [date::add_seconds()](#dateadd_seconds) + - [date::sub_days_from()](#datesub_days_from) + - [date::sub_months_from()](#datesub_months_from) + - [date::sub_years_from()](#datesub_years_from) + - [date::sub_weeks_from()](#datesub_weeks_from) + - [date::sub_hours_from()](#datesub_hours_from) + - [date::sub_minutes_from()](#datesub_minutes_from) + - [date::sub_seconds_from()](#datesub_seconds_from) + - [date::sub_days()](#datesub_days) + - [date::sub_months()](#datesub_months) + - [date::sub_years()](#datesub_years) + - [date::sub_weeks()](#datesub_weeks) + - [date::sub_hours()](#datesub_hours) + - [date::sub_minutes()](#datesub_minutes) + - [date::sub_seconds()](#datesub_seconds) + - [date::format()](#dateformat) +- [Debug](#debug) + - [debug::print_array()](#debugprint_array) + - [debug::print_ansi()](#debugprint_ansi) +- [File](#file) + - [file::make_temp_file()](#filemake_temp_file) + - [file::make_temp_dir()](#filemake_temp_dir) + - [file::name()](#filename) + - [file::basename()](#filebasename) + - [file::extension()](#fileextension) + - [file::dirname()](#filedirname) + - [file::full_path()](#filefull_path) + - [file::mime_type()](#filemime_type) + - [file::contains_text()](#filecontains_text) +- [Format](#format) + - [format::human_readable_seconds()](#formathuman_readable_seconds) + - [format::bytes_to_human()](#formatbytes_to_human) + - [format::strip_ansi()](#formatstrip_ansi) + - [format::text_center()](#formattext_center) + - [format::report()](#formatreport) + - [format::trim_text_to_term()](#formattrim_text_to_term) +- [Interaction](#interaction) + - [interaction::prompt_yes_no()](#interactionprompt_yes_no) + - [interaction::prompt_response()](#interactionprompt_response) +- [Json](#json) + - [json::get_value()](#jsonget_value) +- [Miscellaneous](#miscellaneous) + - [misc::check_internet_connection()](#misccheck_internet_connection) + - [misc::get_pid()](#miscget_pid) + - [misc::get_uid()](#miscget_uid) + - [misc::generate_uuid()](#miscgenerate_uuid) +- [Operating System](#operating-system) + - [os::detect_os()](#osdetect_os) + - [os::detect_linux_distro()](#osdetect_linux_distro) + - [os::detect_linux_version()](#osdetect_linux_version) + - [os::detect_mac_version()](#osdetect_mac_version) +- [String](#string) + - [string::trim()](#stringtrim) + - [string::split()](#stringsplit) + - [string::lstrip()](#stringlstrip) + - [string::rstrip()](#stringrstrip) + - [string::to_lower()](#stringto_lower) + - [string::to_upper()](#stringto_upper) + - [string::contains()](#stringcontains) + - [string::starts_with()](#stringstarts_with) + - [string::ends_with()](#stringends_with) + - [string::regex()](#stringregex) +- [Terminal](#terminal) + - [terminal::is_term()](#terminalis_term) + - [terminal::detect_profile()](#terminaldetect_profile) + - [terminal::clear_line()](#terminalclear_line) +- [Validation](#validation) + - [validation::email()](#validationemail) + - [validation::ipv4()](#validationipv4) + - [validation::ipv6()](#validationipv6) + - [validation::alpha()](#validationalpha) + - [validation::alpha_num()](#validationalpha_num) + - [validation::alpha_dash()](#validationalpha_dash) + - [validation::version_comparison()](#validationversion_comparison) +- [Variable](#variable) + - [variable::is_array()](#variableis_array) + - [variable::is_numeric()](#variableis_numeric) + - [variable::is_int()](#variableis_int) + - [variable::is_float()](#variableis_float) + - [variable::is_bool()](#variableis_bool) + - [variable::is_true()](#variableis_true) + - [variable::is_false()](#variableis_false) + - [variable::is_empty_or_null()](#variableis_empty_or_null) +- [Inspired By](#inspired-by) +- [License](#license) + + +## Installation +The script can be installed and sourced using following methods. + +### Method 1 - Git Submodules +If the library is used inside a git project then git submodules can be used to install the library to the project. +Following command will initialize git submodule and download the library to `./vendor/bash-utility` folder. + +```shell +git submodule init +git submodule add -b master https://github.com/labbots/bash-utility vendor/bash-utility +``` + +To Update submodules to latest code execute the following command. + +```shell +git submodule update --rebase --remote +``` +Once the submodule is added or updated, make sure to commit changes to your repository. + +```shell +git add . +git commit -m 'Added/updated bash-utility library.' +``` +**Note:** When cloning your repository, use `--recurse-submodules` flag to `git clone` command to install the git sub modules. + +### Method 2 - Git Clone +If you don't want to use git submodules, you can use `git clone` to download library and then move the files to desired location manually. + +The below command will clone the repository to `vendor/bash-utility` folder in current working directory. + +```shell +git clone https://github.com/labbots/bash-utility.git ./vendor/bash-utility +``` +### Method 3 - Direct Download +If you do not have git installed, you can download the archive of the latest version of the library. Extract the zip file to appropriate folder by following the below command. + +```shell +wget https://github.com/labbots/bash-utility/archive/master.zip +unzip -q master.zip -d tmp +mkdir -p vendor/bash-utility +mv tmp/bash-utility-master vendor/bash-utility +rm tmp +``` + +## Usage +Bash utility functions can be used by simply sourcing the library script file to your own script. +To access all the functions within the bash-utility library, you could import the main bash file as follows. + +```shell +source "vendor/bash-utility/bash-utility.sh" +``` + +You can also only use the necessary library functions by only importing the required function files. + +```shell +source "vendor/bash-utility/src/array.sh" +``` + + + +## Array + +Functions for array operations and manipulations. + +### array::contains() + +Check if item exists in the given array. + +#### Arguments + +- **$1** (mixed): Item to search (needle). +- **$2** (array): array to be searched (haystack). + +#### Exit codes + +- **0**: If successful. +- **1**: If no match found in the array. +- **2**: Function missing arguments. + +#### Example + +```bash +array=("a" "b" "c") +array::contains "c" ${array[@]} +#Output +0 +``` + +### array::dedupe() + +Remove duplicate items from the array. + +#### Arguments + +- **$1** (array): Array to be deduped. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Deduplicated array. + +#### Example + +```bash +array=("a" "b" "a" "c") +printf "%s" "$(array::dedupe ${array[@]})" +#Output +a +b +c +``` + +### array::is_empty() + +Check if a given array is empty. + +#### Arguments + +- **$1** (array): Array to be checked. + +#### Exit codes + +- **0**: If the given array is empty. +- **2**: If the given array is not empty. + +#### Example + +```bash +array=("a" "b" "c" "d") +array::is_empty "${array[@]}" +``` + +### array::join() + +Join array elements with a string. + +#### Arguments + +- **$1** (string): String to join the array elements (glue). +- **$2** (array): array to be joined with glue string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- String containing a string representation of all the array elements in the same order,with the glue string between each element. + +#### Example + +```bash +array=("a" "b" "c" "d") +printf "%s" "$(array::join "," "${array[@]}")" +#Output +a,b,c,d +printf "%s" "$(array::join "" "${array[@]}")" +#Output +abcd +``` + +### array::reverse() + +Return an array with elements in reverse order. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- The reversed array. + +#### Example + +```bash +array=(1 2 3 4 5) +printf "%s" "$(array::reverse "${array[@]}")" +#Output +5 4 3 2 1 +``` + +### array::random_element() + +Returns a random item from the array. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Random item out of the array. + +#### Example + +```bash +array=("a" "b" "c" "d") +printf "%s\n" "$(array::random_element "${array[@]}")" +#Output +c +``` + +### array::sort() + +Sort an array from lowest to highest. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- sorted array. + +#### Example + +```bash +sarr=("a c" "a" "d" 2 1 "4 5") +array::array_sort "${sarr[@]}" +#Output +1 +2 +4 5 +a +a c +d +``` + +### array::rsort() + +Sort an array in reverse order (highest to lowest). + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- reverse sorted array. + +#### Example + +```bash +sarr=("a c" "a" "d" 2 1 "4 5") +array::array_sort "${sarr[@]}" +#Output +d +a c +a +4 5 +2 +1 +``` + +### array::bsort() + +Bubble sort an integer array from lowest to highest. +This sort does not work on string array. + +#### Arguments + +- **$1** (array): The input array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- bubble sorted array. + +#### Example + +```bash +iarr=(4 5 1 3) +array::bsort "${iarr[@]}" +#Output +1 +3 +4 +5 +``` + +### array::merge() + +Merge two arrays. +Pass the variable name of the array instead of value of the variable. + +#### Arguments + +- **$1** (string): variable name of first array. +- **$2** (string): variable name of second array. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Merged array. + +#### Example + +```bash +a=("a" "c") +b=("d" "c") +array::merge "a[@]" "b[@]" +#Output +a +c +d +c +``` + +## Check + +Helper functions. + +### check::command_exists() + +Check if the command exists in the system. + +#### Arguments + +- **$1** (string): Command name to be searched. + +#### Exit codes + +- **0**: If the command exists. +- **1**: If the command does not exist. +- **2**: Function missing arguments. + +#### Example + +```bash +check::command_exists "tput" +``` + +### check::is_sudo() + +Check if the script is executed with sudo privilege. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If the script is executed with root privilege. +- **1**: If the script is not executed with root privilege + +#### Example + +```bash +check::is_sudo +``` + +## Collection + +(Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function. + +### collection::each() + +Iterates over elements of collection and invokes iteratee for each element. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output of iteratee function. + +#### Example + +```bash +test_func(){ + printf "print value: %s\n" "$1" + return 0 + } +arr1=("a b" "c d" "a" "d") +printf "%s\n" "${arr1[@]}" | collection::each "test_func" +collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach +#output + print value: a b + print value: c d + print value: a + print value: d +``` + +#### Example + +```bash +# If other function from this library is already used to process the array. +# Then following method could be used to pass the array to the function. +out=("$(array::dedupe "${arr1[@]}")") +collection::each "test_func" <<< "${out[@]}" +``` + +### collection::every() + +Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **1**: If iteratee function fails. +- **2**: Function missing arguments. + +#### Example + +```bash +arri=("1" "2" "3" "4") +printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric" +``` + +### collection::filter() + +Iterates over elements of array, returning all elements where iteratee returns true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- array values matching the iteratee function. + +#### Example + +```bash +arri=("1" "2" "3" "a") +printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric" +#output +1 +2 +3 +``` + +### collection::find() + +Iterates over elements of collection, returning the first element where iteratee returns true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Output on stdout + +- first array value matching the iteratee function. + +#### Example + +```bash +arr=("1" "2" "3" "a") +check_a(){ + [[ "$1" = "a" ]] +} +printf "%s\n" "${arr[@]}" | collection::find "check_a" +#Output +a +``` + +### collection::invoke() + +Invokes the iteratee with each element passed as argument to the iteratee. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output from the iteratee function. + +#### Example + +```bash +opt=("-a" "-l") +printf "%s\n" "${opt[@]}" | collection::invoke "ls" +``` + +### collection::map() + +Creates an array of values by running each element in array through iteratee. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. +- other exitcode returned by iteratee. + +#### Output on stdout + +- Output result of iteratee on value. + +#### Example + +```bash +arri=("1" "2" "3") +add_one(){ + i=${1} + i=$(( i + 1 )) + printf "%s\n" "$i" +} +printf "%s\n" "${arri[@]}" | collection::map "add_one" +``` + +### collection::reject() + +The opposite of filter function; this method returns the elements of collection that iteratee does not return true. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- array values not matching the iteratee function. + +#### Example + +```bash +arri=("1" "2" "3" "a") +printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric" +#Ouput +a +``` + +#### See also + +- [collection::filter](#collectionfilter) + +### collection::some() + +Checks if iteratee returns true for any element of the array. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): Iteratee function. + +#### Exit codes + +- **0**: If match successful. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +arr=("a" "b" "3" "a") +printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric" +``` + +## Date + +Functions for manipulating dates. + +### date::now() + +Get current time in unix timestamp. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- current timestamp. + +#### Example + +```bash +echo "$(date::now)" +#Output +1591554426 +``` + +### date::epoc() + +convert datetime string to unix timestamp. + +#### Arguments + +- **$1** (string): date time in any format. + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp for specified datetime. + +#### Example + +```bash +echo "$(date::epoc "2020-07-07 18:38")" +#Output +1594143480 +``` + +### date::add_days_from() + +Add number of days from specified timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_days_from "1594143480")" +#Output +1594229880 +``` + +### date::add_months_from() + +Add number of months from specified timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_months_from "1594143480")" +#Output +1596821880 +``` + +### date::add_years_from() + +Add number of years from specified timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_years_from "1594143480")" +#Output +1625679480 +``` + +### date::add_weeks_from() + +Add number of weeks from specified timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_weeks_from "1594143480")" +#Output +1594748280 +``` + +### date::add_hours_from() + +Add number of hours from specified timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_hours_from "1594143480")" +#Output +1594147080 +``` + +### date::add_minutes_from() + +Add number of minutes from specified timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_minutes_from "1594143480")" +#Output +1594143540 +``` + +### date::add_seconds_from() + +Add number of seconds from specified timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_seconds_from "1594143480")" +#Output +1594143481 +``` + +### date::add_days() + +Add number of days from current day timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_days "1")" +#Output +1591640826 +``` + +### date::add_months() + +Add number of months from current day timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_months "1")" +#Output +1594146426 +``` + +### date::add_years() + +Add number of years from current day timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_years "1")" +#Output +1623090426 +``` + +### date::add_weeks() + +Add number of weeks from current day timestamp. +If number of weeks not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_weeks "1")" +#Output +1592159226 +``` + +### date::add_hours() + +Add number of hours from current day timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_hours "1")" +#Output +1591558026 +``` + +### date::add_minutes() + +Add number of minutes from current day timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_minutes "1")" +#Output +1591554486 +``` + +### date::add_seconds() + +Add number of seconds from current day timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::add_seconds "1")" +#Output +1591554427 +``` + +### date::sub_days_from() + +Subtract number of days from specified timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_days_from "1594143480")" +#Output +1594057080 +``` + +### date::sub_months_from() + +Subtract number of months from specified timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_months_from "1594143480")" +#Output +1591551480 +``` + +### date::sub_years_from() + +Subtract number of years from specified timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_years_from "1594143480")" +#Output +1562521080 +``` + +### date::sub_weeks_from() + +Subtract number of weeks from specified timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_weeks_from "1594143480")" +#Output +1593538680 +``` + +### date::sub_hours_from() + +Subtract number of hours from specified timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_hours_from "1594143480")" +#Output +1594139880 +``` + +### date::sub_minutes_from() + +Subtract number of minutes from specified timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_minutes_from "1594143480")" +#Output +1594143420 +``` + +### date::sub_seconds_from() + +Subtract number of seconds from specified timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. +- **2**: Function missing arguments. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_seconds_from "1594143480")" +#Output +1594143479 +``` + +### date::sub_days() + +Subtract number of days from current day timestamp. +If number of days not specified then it defaults to 1 day. + +#### Arguments + +- **$1** (int): number of days (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_days "1")" +#Output +1588876026 +``` + +### date::sub_months() + +Subtract number of months from current day timestamp. +If number of months not specified then it defaults to 1 month. + +#### Arguments + +- **$1** (int): number of months (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_months "1")" +#Output +1559932026 +``` + +### date::sub_years() + +Subtract number of years from current day timestamp. +If number of years not specified then it defaults to 1 year. + +#### Arguments + +- **$1** (int): number of years (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_years "1")" +#Output +1591468026 +``` + +### date::sub_weeks() + +Subtract number of weeks from current day timestamp. +If number of weeks not specified then it defaults to 1 week. + +#### Arguments + +- **$1** (int): number of weeks (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_weeks "1")" +#Output +1590949626 +``` + +### date::sub_hours() + +Subtract number of hours from current day timestamp. +If number of hours not specified then it defaults to 1 hour. + +#### Arguments + +- **$1** (int): number of hours (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_hours "1")" +#Output +1591550826 +``` + +### date::sub_minutes() + +Subtract number of minutes from current day timestamp. +If number of minutes not specified then it defaults to 1 minute. + +#### Arguments + +- **$1** (int): number of minutes (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_minutes "1")" +#Output +1591554366 +``` + +### date::sub_seconds() + +Subtract number of seconds from current day timestamp. +If number of seconds not specified then it defaults to 1 second. + +#### Arguments + +- **$1** (int): number of seconds (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate timestamp. + +#### Output on stdout + +- timestamp. + +#### Example + +```bash +echo "$(date::sub_seconds "1")" +#Output +1591554425 +``` + +### date::format() + +Format unix timestamp to human readable format. +If format string is not specified then it defaults to "yyyy-mm-dd hh:mm:ss" format. + +#### Arguments + +- **$1** (int): unix timestamp. +- **$2** (string): format control characters based on `date` command (optional). + +#### Exit codes + +- **0**: If successful. +- **1**: If unable to generate time string. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted time string. + +#### Example + +```bash +echo echo "$(date::format "1594143480")" +#Output +2020-07-07 18:38:00 +``` + +## Debug + +Functions to facilitate debugging scripts. + +### debug::print_array() + +Prints the content of array as key value pair for easier debugging. +Pass the variable name of the array instead of value of the variable. + +#### Arguments + +- **$1** (string): variable name of the array. + +#### Output on stdout + +- Formatted key value of array. + +#### Example + +```bash +array=(foo bar baz) +printf "Array\n" +printarr "array" +declare -A assoc_array +assoc_array=([foo]=bar [baz]=foobar) +printf "Assoc Array\n" +printarr "assoc_array" +#Output +Array +0 = foo +1 = bar +2 = baz +Assoc Array +baz = foobar +foo = bar +``` + +### debug::print_ansi() + +Function to print ansi escape sequence as is. +This function helps debug ansi escape sequence in text by displaying the escape codes. + +#### Arguments + +- **$1** (string): input with ansi escape sequence. + +#### Output on stdout + +- Ansi escape sequence printed in output as is. + +#### Example + +```bash +txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" +debug::print_ansi "$txt" +#Output +\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m +``` + +## File + +Functions for handling files. + +### file::make_temp_file() + +Create temporary file. +Function creates temporary file with random name. The temporary file will be deleted when script finishes. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If successful. +- **1**: If failed to create temp file. + +#### Output on stdout + +- file name of temporary file created. + +#### Example + +```bash +echo "$(file::make_temp_file)" +#Output +tmp.vgftzy +``` + +### file::make_temp_dir() + +Create temporary directory. +Function creates temporary directory with random name. The temporary directory will be deleted when script finishes. + +#### Arguments + +- **$1** (string): Temporary directory prefix +- $2 string Flag to auto remove directory on exit trap (true) + +#### Exit codes + +- **0**: If successful. +- **1**: If failed to create temp directory. +- **2**: Missing arguments. + +#### Output on stdout + +- directory name of temporary directory created. + +#### Example + +```bash +echo "$(utility::make_temp_dir)" +#Output +tmp.rtfsxy +``` + +### file::name() + +Get only the filename from string path. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- name of the file with extension. + +#### Example + +```bash +echo "$(file::name "/path/to/test.md")" +#Output +test.md +``` + +### file::basename() + +Get the basename of file from file name. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- basename of the file. + +#### Example + +```bash +echo "$(file::basename "/path/to/test.md")" +#Output +test +``` + +### file::extension() + +Get the extension of file from file name. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **1**: If no extension is found in the filename. +- **2**: Function missing arguments. + +#### Output on stdout + +- extension of the file. + +#### Example + +```bash +echo "$(file::extension "/path/to/test.md")" +#Output +md +``` + +### file::dirname() + +Get directory name from file path. + +#### Arguments + +- **$1** (string): path. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- directory path. + +#### Example + +```bash +echo "$(file::dirname "/path/to/test.md")" +#Output +/path/to +``` + +### file::full_path() + +Get absolute path of file or directory. + +#### Arguments + +- **$1** (string): relative or absolute path to file/direcotry. + +#### Exit codes + +- **0**: If successful. +- **1**: If file/directory does not exist. +- **2**: Function missing arguments. + +#### Output on stdout + +- Absolute path to file/directory. + +#### Example + +```bash +file::full_path "../path/to/file.md" +#Output +/home/labbots/docs/path/to/file.md +``` + +### file::mime_type() + +Get mime type of provided input. + +#### Arguments + +- **$1** (string): relative or absolute path to file/directory. + +#### Exit codes + +- **0**: If successful. +- **1**: If file/directory does not exist. +- **2**: Function missing arguments. +- **3**: If file or mimetype command not found in system. + +#### Output on stdout + +- mime type of file/directory. + +#### Example + +```bash +file::mime_type "../src/file.sh" +#Output +application/x-shellscript +``` + +### file::contains_text() + +Search if a given pattern is found in file. + +#### Arguments + +- **$1** (string): relative or absolute path to file/directory. +- **$2** (string): search key or regular expression. + +#### Exit codes + +- **0**: If given search parameter is found in file. +- **1**: If search paramter not found in file. +- **2**: Function missing arguments. + +#### Example + +```bash +file::contains_text "./file.sh" "^[ @[:alpha:]]*" +file::contains_text "./file.sh" "@file" +#Output +0 +``` + +## Format + +Functions to format provided input. + +### format::human_readable_seconds() + +Format seconds to human readable format. + +#### Arguments + +- **$1** (int): number of seconds. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted time string. + +#### Example + +```bash +echo "$(format::human_readable_seconds "356786")" +#Output +4 days 3 hours 6 minute(s) and 26 seconds +``` + +### format::bytes_to_human() + +Format bytes to human readable format. + +#### Arguments + +- **$1** (int): size in bytes. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted file size string. + +#### Example + +```bash +echo "$(format::bytes_to_human "2250")" +#Output +2.19 KB +``` + +### format::strip_ansi() + +Remove Ansi escape sequences from given text. + +#### Arguments + +- **$1** (string): Input text to be ansi stripped. + +#### Exit codes + +- **0**: If successful. + +#### Output on stdout + +- Ansi stripped text. + +#### Example + +```bash +format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m" +#Output +This is bold red text.This is green text. +``` + +### format::text_center() + +Prints the given text to centre of terminal. + +#### Arguments + +- **$1** (string): Text to be printed. +- **$2** (string): Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted text. + +#### Example + +```bash +format::text_center "This text is in centre of the terminal." "-" +``` + +### format::report() + +Format String to print beautiful report. + +#### Arguments + +- **$1** (string): Text to be printed on the left. +- **$2** (string): Text to be printed within the square brackets. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- formatted text. + +#### Example + +```bash +format::report "Initialising mission state" "Success" +#Output +Initialising mission state ....................................................................[ Success ] +``` + +### format::trim_text_to_term() + +Trim given text to width of the terminal window. + +#### Arguments + +- **$1** (string): Text of first sentence. +- **$2** (string): Text of second sentence (optional). + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- trimmed text. + +#### Example + +```bash +format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence." +#Output +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence. +``` + +## Interaction + +Functions to enable interaction with the user. + +### interaction::prompt_yes_no() + +Prompt yes or no question to the user. + +#### Arguments + +- **$1** (string): The question to be prompted to the user. +- **$2** (string): default answer \[yes/no\] (optional). + +#### Exit codes + +- **0**: If user responds with yes. +- **1**: If user responds with no. +- **2**: Function missing arguments. + +#### Example + +```bash +interaction::prompt_yes_no "Are you sure to proceed" "yes" +#Output +Are you sure to proceed (y/n)? [y] +``` + +### interaction::prompt_response() + +Prompt question to the user. + +#### Arguments + +- **$1** (string): The question to be prompted to the user. +- **$2** (string): default answer (optional). + +#### Exit codes + +- **0**: If user responds with answer. +- **2**: Function missing arguments. + +#### Output on stdout + +- User entered answer to the question. + +#### Example + +```bash +interaction::prompt_response "Choose directory to install" "/home/path" +#Output +Choose directory to install? [/home/path] +``` + +## Json + +Simple json manipulation. These functions does not completely replace `jq` in any way. + +### json::get_value() + +Extract value from json based on key and position. +Input to the function can be a pipe output, here-string or file. + +#### Arguments + +- **$1** (string): id of the field to fetch. +- **$2** (int): position of value to extract.Defaults to 1.(optional) + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- string value of extracted key. + +#### Example + +```bash +json::get_value "id" "1" < json_file +json::get_value "id" <<< "${json_var}" +echo "{\"data\":{\"id\":\"123\",\"value\":\"name string\"}}" | json::get_value "id" +``` + +## Miscellaneous + +Set of miscellaneous helper functions. + +### misc::check_internet_connection() + +Check if internet connection is available. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script can connect to internet. +- **1**: If script cannot access internet. + +#### Example + +```bash +misc::check_internet_connection +``` + +### misc::get_pid() + +Get list of process ids based on process name. + +#### Arguments + +- **$1** (Name): of the process to search. + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- list of process ids. + +#### Example + +```bash +misc::get_pid "chrome" +#Ouput +25951 +26043 +26528 +26561 +``` + +### misc::get_uid() + +Get user id based on username. + +#### Arguments + +- **$1** (username): to search. + +#### Exit codes + +- **0**: If match successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- string uid for the username. + +#### Example + +```bash +misc::get_uid "labbots" +#Ouput +1000 +``` + +### misc::generate_uuid() + +Generate random uuid. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If match successful. + +#### Output on stdout + +- random generated uuid. + +#### Example + +```bash +misc::generate_uuid +#Ouput +65bc64d1-d355-4ffc-a9d9-dc4f3954c34c +``` + +## Operating System + +Functions to detect Operating system and version. + +### os::detect_os() + +Identify the OS the function is run on. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If OS is successfully detected. +- **1**: If unable to detect OS. + +#### Output on stdout + +- Operating system name (linux, mac or windows). + +#### Example + +```bash +os::detect_os +#Output +linux +``` + +### os::detect_linux_distro() + +Identify the distribution flavour of linux. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If Linux distro is successfully detected. +- **1**: If unable to detect OS distro. + +#### Output on stdout + +- Linux OS distribution name (ubuntu, debian, suse, etc.,). + +#### Example + +```bash +os::detect_linux_distro +#Output +ubuntu +``` + +### os::detect_linux_version() + +Identify the Linux version. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If Linux version is successfully detected. +- **1**: If unable to detect Linux version. + +#### Output on stdout + +- Linux OS version number (18.04, 20.04, etc.,). + +#### Example + +```bash +os::detect_linux_version +#Output +20.04 +``` + +### os::detect_mac_version() + +Identify the MacOS version. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If MacOS version is successfully detected. +- **1**: If unable to detect MacOS version. + +#### Output on stdout + +- MacOS version number (10.15.6, etc.,) + +#### Example + +```bash +os::detect_linux_version +#Output +10.15.7 +``` + +## String + +Functions for string operations and manipulations. + +### string::trim() + +Strip whitespace from the beginning and end of a string. + +#### Arguments + +- **$1** (string): The string to be trimmed. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- The trimmed string. + +#### Example + +```bash +echo "$(string::trim " Hello World! ")" +#Output +Hello World! +``` + +### string::split() + +Split a string to array by a delimiter. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The delimiter string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns an array of strings created by splitting the string parameter by the delimiter. + +#### Example + +```bash +array=( $(string::split "a,b,c" ",") ) +printf "%s" "$(string::split "Hello!World" "!")" +#Output +Hello +World +``` + +### string::lstrip() + +Strip characters from the beginning of a string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The characters you want to strip. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the modified string. + +#### Example + +```bash +echo "$(string::lstrip "Hello World!" "He")" +#Output +llo World! +``` + +### string::rstrip() + +Strip characters from the end of a string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The characters you want to strip. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the modified string. + +#### Example + +```bash +echo "$(string::rstrip "Hello World!" "d!")" +#Output +Hello Worl +``` + +### string::to_lower() + +Make a string lowercase. + +#### Arguments + +- **$1** (string): The input string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the lowercased string. + +#### Example + +```bash +echo "$(string::to_lower "HellO")" +#Output +hello +``` + +### string::to_upper() + +Make a string all uppercase. + +#### Arguments + +- **$1** (string): The input string. + +#### Exit codes + +- **0**: If successful. +- **2**: Function missing arguments. + +#### Output on stdout + +- Returns the uppercased string. + +#### Example + +```bash +echo "$(string::to_upper "HellO")" +#Output +HELLO +``` + +### string::contains() + +Check whether the search string exists within the input string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::contains "Hello World!" "lo" +``` + +### string::starts_with() + +Check whether the input string starts with key string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::starts_with "Hello World!" "He" +``` + +### string::ends_with() + +Check whether the input string ends with key string. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::ends_with "Hello World!" "d!" +``` + +### string::regex() + +Check whether the input string matches the given regex. + +#### Arguments + +- **$1** (string): The input string. +- **$2** (string): The search key. + +#### Exit codes + +- **0**: If match found. +- **1**: If no match found. +- **2**: Function missing arguments. + +#### Example + +```bash +string::regex "HELLO" "^[A-Z]*$" +``` + +## Terminal + +Set of useful terminal functions. + +### terminal::is_term() + +Check if script is run in terminal. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script is run on terminal. +- **1**: If script is not run on terminal. + +### terminal::detect_profile() + +Detect profile rc file for zsh and bash of current script running user. + +*Function has no arguments.* + +#### Exit codes + +- **0**: If script is run on terminal. +- **1**: If script is not run on terminal. + +#### Output on stdout + +- path to the profile file. + +### terminal::clear_line() + +Clear the output in terminal on the specified line number. +This function clears line only on terminal. + +#### Arguments + +- **$1** (Line): number to clear. Defaults to 1. (optional) + +#### Exit codes + +- **0**: If script is run on terminal. + +#### Output on stdout + +- clear line ansi code. + +## Validation + +Functions to perform validation on given data. + +### validation::email() + +Validate whether a given input is a valid email address or not. + +#### Arguments + +- **$1** (string): input email address to validate. + +#### Exit codes + +- **0**: If provided input is an email address. +- **1**: If provided input is not an email address. +- **2**: Function missing arguments. + +#### Example + +```bash +test='test@gmail.com' +validation::email "${test}" +echo $? +#Output +0 +``` + +### validation::ipv4() + +Validate whether a given input is a valid IP V4 address. + +#### Arguments + +- **$1** (string): input IPv4 address. + +#### Exit codes + +- **0**: If provided input is a valid IPv4. +- **1**: If provided input is not a valid IPv4. +- **2**: Function missing arguments. + +#### Example + +```bash +ips=' + 4.2.2.2 + a.b.c.d + 192.168.1.1 + 0.0.0.0 + 255.255.255.255 + 255.255.255.256 + 192.168.0.1 + 192.168.0 + 1234.123.123.123 + 0.192.168.1 + ' +for ip in $ips; do + if validation::ipv4 $ip; then stat='good'; else stat='bad'; fi + printf "%-20s: %s\n" "$ip" "$stat" +done +#Output +4.2.2.2 : good +a.b.c.d : bad +192.168.1.1 : good +0.0.0.0 : good +255.255.255.255 : good +255.255.255.256 : bad +192.168.0.1 : good +192.168.0 : bad +1234.123.123.123 : bad +0.192.168.1 : good +``` + +### validation::ipv6() + +Validate whether a given input is a valid IP V6 address. + +#### Arguments + +- **$1** (string): input IPv6 address. + +#### Exit codes + +- **0**: If provided input is a valid IPv6. +- **1**: If provided input is not a valid IPv6. +- **2**: Function missing arguments. + +#### Example + +```bash +ips=' + 2001:db8:85a3:8d3:1319:8a2e:370:7348 + fe80::1ff:fe23:4567:890a + fe80::1ff:fe23:4567:890a%eth2 + 2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar + fezy::1ff:fe23:4567:890a + :: + 2001:db8:: + ' +for ip in $ips; do + if validation::ipv6 $ip; then stat='good'; else stat='bad'; fi + printf "%-50s= %s\n" "$ip" "$stat" +done +#Output +2001:db8:85a3:8d3:1319:8a2e:370:7348 = good +fe80::1ff:fe23:4567:890a = good +fe80::1ff:fe23:4567:890a%eth2 = good +2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar = bad +fezy::1ff:fe23:4567:890a = bad +:: = good +2001:db8:: = good +``` + +### validation::alpha() + +Validate if given variable is entirely alphabetic characters. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is only alpha characters. +- **1**: If input contains any non alpha characters. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abcABC' +validation::alpha "${test}" +echo $? +#Output +0 +``` + +### validation::alpha_num() + +Check if given variable contains only alpha-numeric characters. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is an alpha-numeric. +- **1**: If input is not an alpha-numeric. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abc123' +validation::alpha_num "${test}" +echo $? +#Output +0 +``` + +### validation::alpha_dash() + +Validate if given variable contains only alpha-numeric characters, as well as dashes and underscores. + +#### Arguments + +- **$1** (string): Value of variable to validate. + +#### Exit codes + +- **0**: If input is valid. +- **1**: If input the input is not valid. +- **2**: Function missing arguments. + +#### Example + +```bash +test='abc-ABC_cD' +validation::alpha_dash "${test}" +echo $? +#Output +0 +``` + +### validation::version_comparison() + +Compares version numbers and provides return based on whether the value in equal, less than or greater. + +#### Arguments + +- **$1** (string): Version number to check (eg: 1.0.1) + +#### Exit codes + +- **0**: version number is equal. +- **1**: $1 version number is greater than $2. +- **2**: $1 version number is less than $2. +- **3**: Function is missing required arguments. +- **4**: Provided input argument is in invalid format. + +#### Example + +```bash +test='abc-ABC_cD' +validation::version_comparison "12.0.1" "12.0.1" +echo $? +#Output +0 +``` + +## Variable + +Functions for handling variables. + +### variable::is_array() + +Check if given variable is array. +Pass the variable name instead of value of the variable. + +#### Arguments + +- **$1** (string): name of the variable to check. + +#### Exit codes + +- **0**: If input is array. +- **1**: If input is not an array. + +#### Example + +```bash +arr=("a" "b" "c") +variable::is_array "arr" +#Output +0 +``` + +### variable::is_numeric() + +Check if given variable is a number. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is number. +- **1**: If input is not a number. + +#### Example + +```bash +variable::is_numeric "1234" +#Output +0 +``` + +### variable::is_int() + +Check if given variable is an integer. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is an integer. +- **1**: If input is not an integer. + +#### Example + +```bash +variable::is_int "+1234" +#Output +0 +``` + +### variable::is_float() + +Check if given variable is a float. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is a float. +- **1**: If input is not a float. + +#### Example + +```bash +variable::is_float "+1234.0" +#Output +0 +``` + +### variable::is_bool() + +Check if given variable is a boolean. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is a boolean. +- **1**: If input is not a boolean. + +#### Example + +```bash +variable::is_bool "true" +#Output +0 +``` + +### variable::is_true() + +Check if given variable is a true. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is true. +- **1**: If input is not true. + +#### Example + +```bash +variable::is_true "true" +#Output +0 +``` + +### variable::is_false() + +Check if given variable is false. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is false. +- **1**: If input is not false. + +#### Example + +```bash +variable::is_false "false" +#Output +0 +``` + +### variable::is_empty_or_null() + +Check if given variable is empty or null. + +#### Arguments + +- **$1** (mixed): Value of variable to check. + +#### Exit codes + +- **0**: If input is empty or null. +- **1**: If input is not empty or null. + +#### Example + +```bash +test='' +variable::is_empty_or_null $test +#Output +0 +``` + + + +## Inspired By + +- [Bash Bible](https://github.com/dylanaraps/pure-bash-bible) - A collection of pure bash alternatives to external processes. + +## License + +[MIT](https://github.com/labbots/google-drive-upload/blob/master/LICENSE) diff --git a/lib/bash-utility-master/bash_utility.sh b/lib/bash-utility-master/bash_utility.sh new file mode 100755 index 000000000..65411add0 --- /dev/null +++ b/lib/bash-utility-master/bash_utility.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +source src/array.sh +source src/string.sh +source src/variable.sh +source src/file.sh +source src/misc.sh +source src/date.sh +source src/interaction.sh +source src/check.sh +source src/format.sh +source src/collection.sh +source src/json.sh +source src/terminal.sh +source src/validation.sh +source src/debug.sh +source src/os.sh + + diff --git a/lib/bash-utility-master/bin/bashdoc.awk b/lib/bash-utility-master/bin/bashdoc.awk new file mode 100755 index 000000000..13efdaf7b --- /dev/null +++ b/lib/bash-utility-master/bin/bashdoc.awk @@ -0,0 +1,275 @@ +#!/usr/bin/awk -f + +# Varibles +# style = readme or doc +# toc = true or false +BEGIN { + if (! style) { + style = "doc" + } + + if (! toc) { + toc = 0 + } + + styles["empty", "from"] = ".*" + styles["empty", "to"] = "" + + styles["h1", "from"] = ".*" + styles["h1", "to"] = "# &" + + styles["h2", "from"] = ".*" + styles["h2", "to"] = "## &" + + styles["h3", "from"] = ".*" + styles["h3", "to"] = "### &" + + styles["h4", "from"] = ".*" + styles["h4", "to"] = "#### &" + + styles["h5", "from"] = ".*" + styles["h5", "to"] = "##### &" + + styles["code", "from"] = ".*" + styles["code", "to"] = "```&" + + styles["/code", "to"] = "```" + + styles["argN", "from"] = "^(\\$[0-9]) (\\S+)" + styles["argN", "to"] = "**\\1** (\\2):" + + styles["arg@", "from"] = "^\\$@ (\\S+)" + styles["arg@", "to"] = "**...** (\\1):" + + styles["li", "from"] = ".*" + styles["li", "to"] = "- &" + + styles["i", "from"] = ".*" + styles["i", "to"] = "*&*" + + styles["anchor", "from"] = ".*" + styles["anchor", "to"] = "[&](#&)" + + styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" + styles["exitcode", "to"] = "**\\1**: \\2" + + styles["h_rule", "to"] = "---" + + styles["comment", "from"] = ".*" + styles["comment", "to"] = "" + + + output_format["readme", "h1"] = "h2" + output_format["readme", "h2"] = "h3" + output_format["readme", "h3"] = "h4" + output_format["readme", "h4"] = "h5" + + output_format["bashdoc", "h1"] = "h1" + output_format["bashdoc", "h2"] = "h2" + output_format["bashdoc", "h3"] = "h3" + output_format["bashdoc", "h4"] = "h4" + + output_format["webdoc", "h1"] = "empty" + output_format["webdoc", "h2"] = "h3" + output_format["webdoc", "h3"] = "h4" + output_format["webdoc", "h4"] = "h5" + +} + +function render(type, text) { + if((style,type) in output_format){ + type = output_format[style,type] + } + return gensub( \ + styles[type, "from"], + styles[type, "to"], + "g", + text \ + ) +} + +function render_list(item, anchor) { + return "- [" item "](#" anchor ")" +} + +function generate_anchor(text) { + # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 + text = tolower(text) + gsub(/[^[:alnum:]_ -]/, "", text) + gsub(/ /, "-", text) + return text +} + +function reset() { + has_example = 0 + has_args = 0 + has_exitcode = 0 + has_stdout = 0 + + content_desc = "" + content_example = "" + content_args = "" + content_exitcode = "" + content_seealso = "" + content_stdout = "" +} + +/^[[:space:]]*# @internal/ { + is_internal = 1 +} + +/^[[:space:]]*# @file/ { + sub(/^[[:space:]]*# @file /, "") + + filedoc = render("h1", $0) "\n" + if(style == "webdoc"){ + filedoc = filedoc render("comment", "file=" $0) "\n" + } + +} + +/^[[:space:]]*# @brief/ { + sub(/^[[:space:]]*# @brief /, "") + if(style == "webdoc"){ + filedoc = filedoc render("comment", "brief=" $0) "\n" + } + filedoc = filedoc "\n" $0 +} + +/^[[:space:]]*# @description/ { + in_description = 1 + in_example = 0 + + reset() + + docblock = "" +} + +in_description { + if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) { + if (!match(content_desc, /\n$/)) { + content_desc = content_desc "\n" + } + in_description = 0 + } else { + sub(/^[[:space:]]*# @description /, "") + sub(/^[[:space:]]*# /, "") + sub(/^[[:space:]]*#$/, "") + + content_desc = content_desc "\n" $0 + } +} + +in_example { + + if (! /^[[:space:]]*#[ ]{3}/) { + + in_example = 0 + + content_example = content_example "\n" render("/code") "\n" + } else { + sub(/^[[:space:]]*#[ ]{3}/, "") + + content_example = content_example "\n" $0 + } +} + +/^[[:space:]]*# @example/ { + in_example = 1 + content_example = content_example "\n" render("h3", "Example") + content_example = content_example "\n\n" render("code", "bash") +} + +/^[[:space:]]*# @arg/ { + if (!has_args) { + has_args = 1 + + content_args = content_args "\n" render("h3", "Arguments") "\n\n" + } + + sub(/^[[:space:]]*# @arg /, "") + + $0 = render("argN", $0) + $0 = render("arg@", $0) + + content_args = content_args render("li", $0) "\n" +} + +/^[[:space:]]*# @noargs/ { + content_args = content_args "\n" render("i", "Function has no arguments.") "\n" +} + +/^[[:space:]]*# @exitcode/ { + if (!has_exitcode) { + has_exitcode = 1 + + content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n" + } + + sub(/^[[:space:]]*# @exitcode /, "") + + $0 = render("exitcode", $0) + + content_exitcode = content_exitcode render("li", $0) "\n" +} + +/^[[:space:]]*# @see/ { + sub(/[[:space:]]*# @see /, "") + anchor = generate_anchor($0) + $0 = render_list($0, anchor) + + content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n" +} + +/^[[:space:]]*# @stdout/ { + has_stdout = 1 + + sub(/^[[:space:]]*# @stdout /, "") + + content_stdout = content_stdout "\n" render("h3", "Output on stdout") + content_stdout = content_stdout "\n\n" render("li", $0) "\n" +} + +{ + docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso + if(style == "webdoc"){ + docblock = docblock "\n" render("h_rule") "\n" + } +} + +/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example { + if (is_internal) { + is_internal = 0 + } else { + func_name = gensub(\ + /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \ + "\\3()", \ + "g" \ + ) + doc = doc "\n" render("h2", func_name) "\n" docblock + if (toc) { + url = generate_anchor(func_name) + + content_idx = content_idx "\n" "- [" func_name "](#" url ")" + } + } + + docblock = "" + reset() +} + +END { + if (filedoc != "") { + print filedoc + } + + if (toc) { + print "" + print render("h2", "Table of Contents") + print content_idx + print "" + print render("h_rule") + } + + print doc +} diff --git a/lib/bash-utility-master/bin/generate_readme.sh b/lib/bash-utility-master/bin/generate_readme.sh new file mode 100755 index 000000000..858a4b8be --- /dev/null +++ b/lib/bash-utility-master/bin/generate_readme.sh @@ -0,0 +1,400 @@ +#!/usr/bin/env bash + +#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh +#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh +_usage() { + printf " +Script to autogenerate markdown based on bash source code.\n +The script generates table of contents and bashdoc and update the given markdown file.\n +Usage:\n %s [options.. ]\n +Options:\n + -f | --file - Relative or absolute path to the README.md file. + -s | --sh-dir - path to the bash script source folder to generate shdocs.\n + -l | --toc-level - Minimum level of header to print in Table of Contents.\n + -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n + -w | --webdoc - Flag to indicate generation of webdoc.\n + -p | --dest-dir - Path in which wedoc files must be generated.\n + -h | --help - Display usage instructions.\n" "${0##*/}" + exit 0 +} + +_setup_arguments() { + + unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR + MINLEVEL=1 + MAXLEVEL=3 + SCRIPT_FILE="${0##*/}" + declare source="${BASH_SOURCE[0]}" + while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink + SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" + source="$(readlink "$source")" + [[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located + done + SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)" + SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md" + SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src" + WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions" + + SHORTOPTS="whp:f:m:d:s:-:" + + while getopts "${SHORTOPTS}" OPTION; do + case "${OPTION}" in + -) + _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } + case "${OPTARG}" in + help) + _usage + ;; + file) + _check_longoptions "${!OPTIND}" + SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-level) + _check_longoptions "${!OPTIND}" + MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-depth) + _check_longoptions "${!OPTIND}" + MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + sh-dir) + _check_longoptions "${!OPTIND}" + SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + webdoc) + WEBDOC=true + ;; + dest-dir) + WEBDOC_DEST_DIR="${OPTARG}" + ;; + '') + _usage + ;; + *) + printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + ;; + h) + _usage + ;; + f) + SOURCE_MARKDOWN="${OPTARG}" + ;; + m) + MINLEVEL="${OPTARG}" + ;; + d) + MAXLEVEL="${OPTARG}" + ;; + s) + SOURCE_SCRIPT_DIR="${OPTARG}" + ;; + w) + WEBDOC=true + ;; + p) + WEBDOC_DEST_DIR="${OPTARG}" + ;; + :) + printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + ?) + printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + done + shift "$((OPTIND - 1))" + + if [[ -w "${SOURCE_MARKDOWN}" ]]; then + declare src_file src_extension + src_file="${SOURCE_MARKDOWN##*/}" + src_extension="${src_file##*.}" + if [[ "${src_extension,,}" != "md" ]]; then + printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 + fi + else + printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 + fi + + if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then + printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 + fi + + declare re='^[0-9]+$' + if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then + echo "error: Not a number" >&2 + exit 1 + fi + if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then + printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 + fi + + [ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}" + +} + +_setup_tempfile() { + declare temp_file + type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; } + trap 'rm -f "${temp_file}"' EXIT + printf "%s" "${temp_file}" +} + +_generate_shdoc() { + declare file + file="$(realpath "${1}")" + if [[ -s "${file}" ]]; then + awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2" + #awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md" + fi +} + +_insert_shdoc_to_file() { + declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc + shdoc_tmp_file="$1" + source_markdown="$2" + + start_shdoc="" + info_shdoc="" + end_shdoc="" + + sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" + + if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then + # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file + + sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" + echo -e "Updated bashdoc content to ${source_markdown} successfully\n" + + else + { + printf "%s\n" "${start_shdoc}" + cat "${shdoc_tmp_file}" + printf "%s\n" "${end_shdoc}" + } >> "${source_markdown}" + echo -e "Created bashdoc content to ${source_markdown} successfully\n" + fi +} + +_process_sh_files() { + declare shdoc_tmp_file source_script_dir source_markdown + source_markdown="${1}" + source_script_dir="${2}" + shdoc_tmp_file=$(_setup_tempfile) + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + while IFS= read -r -d '' line; do + _generate_shdoc "${line}" "${shdoc_tmp_file}" + done + _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" + rm "${shdoc_tmp_file}" + +} + +_generate_toc() { + + declare line level title anchor output counter temp_output invalid_chars + + invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" + while IFS='' read -r line || [[ -n "${line}" ]]; do + level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" + title="$(echo "${line}" | sed -E 's/^#+ //')" + [[ "${title}" = "Table of Contents" ]] && continue + + # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text + anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" + + # check new line introduced is not duplicated, if is duplicated, introduce a number at the end + temp_output=$output"$level- [$title](#$anchor)\n" + counter=1 + while true; do + nlines="$(echo -e "${temp_output}" | wc -l)" + duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)" + if [ "${nlines}" = "${duplines}" ]; then + break + fi + temp_output=$output"$level- [$title](#$anchor-$counter)\n" + counter=$((counter + 1)) + done + + output="$temp_output" + + # grep: filter header candidates to be included in toc + # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment + done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")" + + # when in toc we have two `--` quit one + echo "$output" + +} + +_insert_toc_to_file() { + + declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash + source_markdown="${1}" + toc_text="${2}" + start_toc="" + info_toc="" + end_toc="" + + toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" + # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command + utext_ampersand="id8234923000230gzz" + utext_slash="id9992384923423gzz" + toc_block="${toc_block//\&/${utext_ampersand}}" + toc_block="${toc_block//\//${utext_slash}}" + + # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 + # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches + if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then + # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace + sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" + echo -e "Updated TOC content in ${source_markdown} succesfully\n" + + else + sed -i 1i"$toc_block" "${source_markdown}" + echo -e "Created TOC in ${source_markdown} succesfully\n" + + fi + + # undo symbol replacements + sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" + sed -i "s,${utext_slash},\/,g" "${source_markdown}" + +} + +_process_toc() { + declare toc_temp_file source_markdown level toc_text + source_markdown="${1}" + + toc_temp_file=$(_setup_tempfile) + + sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" + + level=$MINLEVEL + while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do + level=$((level + 1)) + done + + MINLEVEL=${level} + toc_text=$(_generate_toc "${toc_temp_file}") + rm "${toc_temp_file}" + + _insert_toc_to_file "${source_markdown}" "${toc_text}" +} + +_generate_webdoc() { + declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file + declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc + declare webdoc_lastmod_date webdoc_lastmod_epoc + file="$(realpath "${1}")" + dest_dir="${2}" + + filename="${file##*/}" + file_basename="${filename%.*}" + dest_file_path="${dest_dir}/${file_basename}.md" + file_modified_date="$(date -r "${file}" +"%FT%T%:z")" + file_modified_date_epoc="$(date -r "${file}" +"%s")" + + start_shdoc="" + end_shdoc="" + if [[ ! -f "${dest_file_path}" ]]; then + + cat << EOF > "${dest_file_path}" +--- +title : +description : +date : ${file_modified_date} +lastmod : ${file_modified_date} +--- +${start_shdoc} +${end_shdoc} +EOF + is_new_file=true + else + is_new_file=false + webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")" + webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")" + fi + + if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then + + shdoc_tmp_file=$(_setup_tempfile) + if [[ -s "${file}" ]]; then + awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}" + fi + + if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then + sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}" + fi + + # Extract title and description from webdoc + title="$(sed -ne 's/-->//; s/^.*//; s/^.*/${title}/g" "${dest_file_path}" + sed -i -e "s//${description}/g" "${dest_file_path}" + else + sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}" + sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}" + + fi + + # Update the last modified timestamp in front matter + sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}" + + echo -e "Updated bashdoc content to ${dest_file_path} successfully." + rm "${shdoc_tmp_file}" + + fi +} +_process_webdoc_files() { + declare source_script_dir dest_dir + + source_script_dir="${1}" + dest_dir="${2}" + + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + while IFS= read -r -d '' line; do + _generate_webdoc "${line}" "${dest_dir}" + done +} + +_count_library_functions() { + declare source_script_dir + source_script_dir="${1}" + + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | + { + declare -i function_count=0 count=0 + while IFS= read -r -d '' line; do + count=0 + count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}") + function_count=$((function_count + count)) + done + printf "Total library functions: %s \n" "${function_count}" + + } +} +main() { + # export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' + # set -x + trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT + set -o errexit -o noclobber -o pipefail + + _setup_arguments "${@}" + _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" + _process_toc "${SOURCE_MARKDOWN}" + + if [[ -n ${WEBDOC} ]]; then + _process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}" + fi + _count_library_functions "${SOURCE_SCRIPT_DIR}" +} + +main "${@}" diff --git a/lib/bash-utility-master/image/bash-utility.png b/lib/bash-utility-master/image/bash-utility.png new file mode 100644 index 0000000000000000000000000000000000000000..c1bfb8b556809ce645cf41ab6d4b11547df68fc7 GIT binary patch literal 32396 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 literal 0 HcmV?d00001 diff --git a/lib/bash-utility/array.sh b/lib/bash-utility-master/src/array.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/array.sh rename to lib/bash-utility-master/src/array.sh diff --git a/lib/bash-utility/check.sh b/lib/bash-utility-master/src/check.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/check.sh rename to lib/bash-utility-master/src/check.sh diff --git a/lib/bash-utility/collection.sh b/lib/bash-utility-master/src/collection.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/collection.sh rename to lib/bash-utility-master/src/collection.sh diff --git a/lib/bash-utility/date.sh b/lib/bash-utility-master/src/date.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/date.sh rename to lib/bash-utility-master/src/date.sh diff --git a/lib/bash-utility/debug.sh b/lib/bash-utility-master/src/debug.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/debug.sh rename to lib/bash-utility-master/src/debug.sh diff --git a/lib/bash-utility/file.sh b/lib/bash-utility-master/src/file.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/file.sh rename to lib/bash-utility-master/src/file.sh diff --git a/lib/bash-utility/format.sh b/lib/bash-utility-master/src/format.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/format.sh rename to lib/bash-utility-master/src/format.sh diff --git a/lib/bash-utility/interaction.sh b/lib/bash-utility-master/src/interaction.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/interaction.sh rename to lib/bash-utility-master/src/interaction.sh diff --git a/lib/bash-utility/json.sh b/lib/bash-utility-master/src/json.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/json.sh rename to lib/bash-utility-master/src/json.sh diff --git a/lib/bash-utility/misc.sh b/lib/bash-utility-master/src/misc.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/misc.sh rename to lib/bash-utility-master/src/misc.sh diff --git a/lib/bash-utility/os.sh b/lib/bash-utility-master/src/os.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/os.sh rename to lib/bash-utility-master/src/os.sh diff --git a/lib/bash-utility/string.sh b/lib/bash-utility-master/src/string.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/string.sh rename to lib/bash-utility-master/src/string.sh diff --git a/lib/bash-utility/terminal.sh b/lib/bash-utility-master/src/terminal.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/terminal.sh rename to lib/bash-utility-master/src/terminal.sh diff --git a/lib/bash-utility/validation.sh b/lib/bash-utility-master/src/validation.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/validation.sh rename to lib/bash-utility-master/src/validation.sh diff --git a/lib/bash-utility/variable.sh b/lib/bash-utility-master/src/variable.sh old mode 100644 new mode 100755 similarity index 100% rename from lib/bash-utility/variable.sh rename to lib/bash-utility-master/src/variable.sh diff --git a/lib/config/benchymark.sh b/lib/config/benchymark.sh deleted file mode 100644 index 1882c8812..000000000 --- a/lib/config/benchymark.sh +++ /dev/null @@ -1,57 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Benchmark related functions. See -# https://systemd.io/ https://www.7-zip.org/ for more info. - -# @description system boot-up performance statistics. -# -# @example -# benchymark::see_systemd $1 (-h) -# #Output -# armbianmonitor help list options.) -# -# @exitcode 0 If successful. -# -# @stdout tobd. -benchymark::see_monitor(){ - [[ $1 == "" ]] && clear && armbianmonitor -h ; - [[ $1 == $1 ]] && armbianmonitor "$1" ; - exit 0 - } -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Benchmark related functions. See -# https://systemd.io/ for more info. -# https://www.7-zip.org/ - -# @description system boot-up performance statistics. -# -# @example -# benchymark::see_systemd $1 (blame time) -# #Output -# -h (systemd help list, not not all are avalible.) -# time (quick check of boot time) -# balme (Lists modual boot load times) -# -# @exitcode 0 If successful. -# -# @stdout tobd. -benchymark::see_boot_times(){ - - [[ $1 == "" ]] && sys_blame=$( systemd-analyze -h ) ; - [[ $1 == "blame" ]] && sys_blame=$( systemd-analyze blame ) ; - [[ $1 == "time" ]] && sys_blame=$( systemd-analyze time ) ; - printf '%s\n' "${sys_blame[@]}" - exit 0 - } diff --git a/lib/config/cpu.sh b/lib/config/cpu.sh deleted file mode 100644 index 6847be043..000000000 --- a/lib/config/cpu.sh +++ /dev/null @@ -1,199 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# CPU related functions. See https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt for more info. - -# @description Return policy as int based on original armbian-config logic. -# -# @example -# cpu::get_policy -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -# -# @stdout Policy as integer. -cpu::see_policy(){ - declare -i policy=0 - [[ $(grep -c '^processor' /proc/cpuinfo) -gt 4 ]] && policy=4 - [[ ! -d /sys/devices/system/cpu/cpufreq/policy4 ]] && policy=0 - [[ -d /sys/devices/system/cpu/cpufreq/policy0 && -d /sys/devices/system/cpu/cpufreq/policy2 ]] && policy=2 - printf '%d\n' "$policy" -} - -# @description Return CPU frequencies as string delimited by space. -# -# @example -# cpu::get_freqs 0 -# echo $? -# #Output -# 648000 816000 912000 960000 1008000 1056000 1104000 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout Space delimited string of CPU frequencies. -cpu::see_freqs(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_frequencies" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU minimum frequency as string. -# -# @example -# cpu::get_min_freq 0 -# echo $? -# #Output -# 648000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 1 If file not found. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU minimum frequency as string. -cpu::see_min_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_min_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU maximum frequency as string. -# -# @example -# cpu::get_max_freq 0 -# echo $? -# #Output -# 1152000 -# -# @arg $1 int policy. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# -# @stdout CPU maximum frequency as string. -cpu::see_max_freq(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_max_freq" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governor as string. -# -# @example -# cpu::get_governor 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::see_governor(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_governor" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Return CPU governors as string delimited by space. -# -# @example -# cpu::get_governors 0 -# echo $? -# #Output -# performance -# -# @arg $1 int policy. -cpu::see_governors(){ - # Check number of arguments - [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/sys/devices/system/cpu/cpufreq/policy$1/scaling_available_governors" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - # Return value - printf '%s\n' "$(cat $file)" -} - -# @description Set min, max and CPU governor. -# -# @example -# cpu::set_freq 0 648000 1152000 performance -# echo $? -# #Output -# performance -# -# @arg $1 int Policy. -# @arg $2 int Minimum frequency. -# @arg $3 int Maximum frequency. -# @arg $4 string Governor. -# -# @exitcode 0 If successful. -# @exitcode 2 Function missing arguments. -# @exitcode 3 Invalid minimum frequency. -# @exitcode 4 Invalid maximum frequency. -# @exitcode 5 Minimum frequency must be <= maximum frequency. -# @exitcode 6 Invalid governor. -cpu::set_freq(){ - # Check number of arguments - [[ $# -lt 4 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2 - # Build file based on policy value - local file="/etc/default/cpufrequtils" - # Check if file exists - [ ! -f "$file" ] && printf '%s\n' "$file not found" && return 1 - declare -i policy=$1 - declare -i min_freq=$2 - declare -i max_freq=$3 - local governor=$4 - # Return frequencies as array - declare -a freqs=( $(string::split "$(cpu::get_freqs $policy)" " ") ) - # Validate minimum frequency - array::contains "$min_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 3 - # Validate maximum frequency - array::contains "$max_freq" ${freqs[@]} - [[ $? != 0 ]] && printf "%s: Invalid maximum frequency\n" "${FUNCNAME[0]}" && return 4 - # Validate minimum frequency is <= maximum frequency - [ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5 - # Return governors - declare -a governors=( $(string::split "$(cpu::get_governors $policy)" " ") ) - # Validate maximum governor - array::contains "$governor" ${governor[@]} - [[ $? != 0 ]] && printf "%s: Invalid minimum frequency\n" "${FUNCNAME[0]}" && return 6 - # Update file - sed -i "s/MIN_SPEED=.*/MIN_SPEED=$min_freq/" "$file" - sed -i "s/MAX_SPEED=.*/MAX_SPEED=$max_freq/" "$file" - sed -i "s/GOVERNOR=.*/GOVERNOR=$governor/" "$file" - # Return value - return 0 -} - diff --git a/lib/config/desktops.sh b/lib/config/desktops.sh deleted file mode 100644 index 9852e796d..000000000 --- a/lib/config/desktops.sh +++ /dev/null @@ -1,25 +0,0 @@ -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Desktop setup related functions. See *(todo)* for more info. - -# @description Display a list of avalible desktops to install. -# -# @example -# desktops::see_desktops -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -# -# @stdout list of avalible desktops. -desktops::see_desktops(){ - - apt-cache search armbian-$(grep VERSION_CODENAME /etc/os-release | cut -d"=" -f2)-desktop- | cut -d" " -f1 - - } \ No newline at end of file diff --git a/lib/config/extradrives.sh b/lib/config/extradrives.sh deleted file mode 100644 index dad7a0639..000000000 --- a/lib/config/extradrives.sh +++ /dev/null @@ -1,80 +0,0 @@ - -# -# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. -# -# Externa Drive related functions. See -# http://linux-mtd.infradead.org/doc/general.html for more info. -# https://en.wikipedia.org/wiki/MultiMediaCard#eMMC - -# @description Set up a simulated MTD spi flash for testing. -# -# @example -# extradrives::set_spi_vflash -# echo $? -# #Output -# /dev/mtd0 -# /dev/mtd0ro -# /dev/mtdblock0 -# -# @exitcode 0 If successful. -extradrives::set_spi_vflash(){ - - # Load the nandsim and mtdblock modules to create a virtual MTD device - - sudo modprobe mtdblock - #sudo modprobe nandsim - # Find the newly created MTD device - if [[ ! -e /dev/mtdblock0 ]]; then - sudo modprobe nandsim - irtual_mtd=$(grep -l "NAND simulator" /sys/class/mtd/mtd*/name | sed -r 's/.*mtd([0-9]+).*/mtd\1/') - else - echo "$( sudo ls /dev/mtdblock0 )" - fi - - # Create a symlink to the virtual MTD device with the name "spi0.0" - # This is necessary because the erase_spi_bootloader function looks for an MTD device with this name - if [[ ! -e /dev/mtdblock0 ]]; then - ln -s /dev/$virtual_mtd /dev/mtdblock0 - fi - - # Create the mount point if it doesn't exist - mkdir -p /tmp/boot - - # Mount the virtual MTD device to the mount point - mount -t jffs2 /dev/mtdblock0 /tmp/boot - - # write a file to remove - touch /tmp/boot/Mounted_MTD.txt - - echo "$( sudo ls /dev/mtd* )" - -} - - -# @description Remove tsting simulated MTD spi flash. -# -# @example -# extradrives::rem_spi_vflash -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -extradrives::rem_spi_vflash(){ - - # Unmount the virtual MTD device from the mount point - umount $(mount | grep /dev/mtdblock0 | awk '{print $3}') - - # Remove the symlink to the virtual MTD device - rm /dev/mtdblock0 - - # Unload the nandsim and mtdblock modules to remove the virtual MTD device - sudo modprobe -r mtdblock - sudo modprobe -r nandsim - - echo "0" -} diff --git a/lib/config/iolocal.sh b/lib/config/iolocal.sh deleted file mode 100644 index 0fd1e263f..000000000 --- a/lib/config/iolocal.sh +++ /dev/null @@ -1,57 +0,0 @@ - -# @description Enable or Disable Infrared Remote Control support. -# -# @example -# io::set_ir_toggle enable -# io::set_ir_toggle disable -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -iolocal::set_ir_toggle(){ - -[[ "$1" == "enable" ]] && sudo apt -y --no-install-recommends install lirc ; exit 0 ; -[[ "$1" == "disabe" ]] && sudo apt -y remove lirc ; sudo apt -y -qq autoremove ; exit 0 ; - -} - -# @description See a list of board led options. -# -# @example -# boardled::set_sysled -# #Output -# Led blinks to set $1 -# -# @exitcode 0 If successful. -# -# @stdout tbd. -boardled::see_sysled_opt(){ - - # the avalible options - readarray triggers_led < <( cat /sys/class/leds/*/trigger ) - # see pass not argument the avalible options - [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; - exit 0 -} - -# @description See a list of board led options. -# -# @example -# boardled::set_sysled -# #Output -# Led blinks to set $1 -# -# @exitcode 0 If successful. -# -# @stdout tbd. -boardled::set_sysled(){ - - # the avalible options - readarray triggers_led < <( cat /sys/class/leds/*/trigger ) - # see pass not argument the avalible options - [[ -z $1 ]] && printf "%s\n" "${triggers_led[@]} "; - # Set the systme Led blink to $1 valus - [[ " ${triggers_led[@]} " =~ " ${1} " ]] && echo "${1}"| sudo tee /sys/class/leds/bananapi-m2-zero:red:pwr/trigger ; - -} diff --git a/lib/config/ioremote.sh b/lib/config/ioremote.sh deleted file mode 100644 index dac2c3de3..000000000 --- a/lib/config/ioremote.sh +++ /dev/null @@ -1,19 +0,0 @@ - -# @description Enable or Disable remote IO devices. (eth, urt, ssh, ... ?) -# -# @example -# io::set_toggle enable -# io::set_toggle disable -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -ioremote::set_toggle(){ - -[[ "$1" == "" ]] && echo "enable\ndisable"; -[[ "$1" == "enable" ]] && echo "ToDo; enable place holder" ; -[[ "$1" == "disabe" ]] && echo "ToDo; disable place holder" ; - -exit 0 -} \ No newline at end of file diff --git a/lib/config/iowirless.sh b/lib/config/iowirless.sh deleted file mode 100644 index 5858229a9..000000000 --- a/lib/config/iowirless.sh +++ /dev/null @@ -1,18 +0,0 @@ - -# @description Enable or Disable wireless IO devices. (wifi, bluetooth, ... lora?) -# -# @example -# io::set_toggle enable -# io::set_toggle disable -# echo $? -# #Output -# 0 -# -# @exitcode 0 If successful. -iowireless::set_toggle(){ - -[[ "$1" == "" ]] && echo "enable\ndisable"; -[[ "$1" == "enable" ]] && echo "ToDo; enable place holder" ; -[[ "$1" == "disabe" ]] && echo "ToDo; disable place holder" ; - -} \ No newline at end of file diff --git a/share/armbian-configng/armbian-configng.svg b/share/armbian-configng/armbian-configng.svg new file mode 100644 index 000000000..4d19784d6 --- /dev/null +++ b/share/armbian-configng/armbian-configng.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/share/armbian-configng/data/armbian-configng.csv b/share/armbian-configng/data/armbian-configng.csv new file mode 100755 index 000000000..ee79cbe10 --- /dev/null +++ b/share/armbian-configng/data/armbian-configng.csv @@ -0,0 +1,4 @@ +Function Name,Group Name,Description,Options,Category,Category Description +NMTUI,network,Network Manager.,none.,network,Network Wired wireless Bluetooth access point +Hello,system,Hello System.,none,system,System and Security +Bencharking,monitor,Armbian Monitor and Bencharking.,,system,System and Security diff --git a/share/armbian-configng/data/armbian-configng.json b/share/armbian-configng/data/armbian-configng.json new file mode 100755 index 000000000..e3a2fea3d --- /dev/null +++ b/share/armbian-configng/data/armbian-configng.json @@ -0,0 +1,26 @@ +[ + { + "Function Name": "NMTUI", + "Group Name": "network", + "Description": "Network Manager.", + "Options": "none.", + "Category": "network", + "Category Description": "Network Wired wireless Bluetooth access point" + }, + { + "Function Name": "Hello", + "Group Name": "system", + "Description": "Hello System.", + "Options": "none", + "Category": "system", + "Category Description": "System and Security" + }, + { + "Function Name": "Bencharking", + "Group Name": "monitor", + "Description": "Armbian Monitor and Bencharking.", + "Options": "", + "Category": "system", + "Category Description": "System and Security" + } +] diff --git a/share/armbian-configng/index.html b/share/armbian-configng/index.html new file mode 100755 index 000000000..d5e3ccce4 --- /dev/null +++ b/share/armbian-configng/index.html @@ -0,0 +1,90 @@ + + + + + + Armbian index + + + +
+

index

+
+ +
+
+ +
+
+ + + + + + From 1994b8a53d7bc6d508c124b141257b8c7ce44660 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Wed, 6 Dec 2023 20:39:18 +0100 Subject: [PATCH 32/48] Add call to Armbian installer --- .../system/armbian-install.sh | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lib/armbian-configng/system/armbian-install.sh diff --git a/lib/armbian-configng/system/armbian-install.sh b/lib/armbian-configng/system/armbian-install.sh new file mode 100644 index 000000000..688448636 --- /dev/null +++ b/lib/armbian-configng/system/armbian-install.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Copyright (c) Authors: http://www.armbian.com/authors, info@armbian.com +# +# This file is licensed under the terms of the GNU General Public +# License version 2. This program is licensed "as is" without any +# warranty of any kind, whether express or implied. + + +# @description Armbian installer +# +# @exitcode 0 If successful. +# +# @options none. +# +function system::Install(){ + + armbian-install + return 0 +} + From 9389784d185e67ecd2254c52ffb346b7064f08b3 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Wed, 6 Dec 2023 20:45:49 +0100 Subject: [PATCH 33/48] Add to readme and change the name of script --- README.md | 9 ++++++++- .../system/{armbian-install.sh => armbian_install.sh} | 0 2 files changed, 8 insertions(+), 1 deletion(-) rename lib/armbian-configng/system/{armbian-install.sh => armbian_install.sh} (100%) diff --git a/README.md b/README.md index f7fb4dc29..69fb59e52 100755 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ function group::string() {s - [CSV](share/armbian-configng/data/armbian-configng.csv) - [HTML](share/armbian-configng/armbian-configng-table.html) - [github.io](//tearran/github.io/armbian-configng/index.html) -## Functions list as of 2023-12-05 +## Functions list as of 2023-12-06 ## network System and Security @@ -56,6 +56,13 @@ System and Security ## system Network Wired wireless Bluetooth access point +### armbian_install.sh + + - **Group Name:** system + - **Action Name:** Install + - **Options:** none + - **Description:** Armbian installer. + ### hello_world.sh - **Group Name:** system diff --git a/lib/armbian-configng/system/armbian-install.sh b/lib/armbian-configng/system/armbian_install.sh similarity index 100% rename from lib/armbian-configng/system/armbian-install.sh rename to lib/armbian-configng/system/armbian_install.sh From 3ba18edd2bd62d902ba03038ba358adb38aa5df3 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Wed, 6 Dec 2023 20:53:00 +0100 Subject: [PATCH 34/48] Add action scripts --- .github/workflows/debian.yml | 31 +++++++++++++++++++++++++++++++ .github/workflows/lint.yml | 26 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .github/workflows/debian.yml create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml new file mode 100644 index 000000000..0a7df186b --- /dev/null +++ b/.github/workflows/debian.yml @@ -0,0 +1,31 @@ +name: Debian package +# +# Description +# + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + + Debian: + uses: armbian/scripts/.github/workflows/pack-debian.yml@master + with: + matrix: "all:jammy" + maintainer: "Igor Pecovnik " + package: "configng" + licence: "GPL 2.0" + homepage: "https://www.armbian.com" + section: "default" + priority: "optional" + depends: "" + description: "Configng" + + secrets: + GPG_PRIVATE_KEY: ${{ secrets.GPG_KEY1 }} + PASSPHRASE: ${{ secrets.GPG_PASSPHRASE1 }} + SSH_KEY_TORRENTS: ${{ secrets.KEY_TORRENTS }} + KNOWN_HOSTS_UPLOAD: ${{ secrets.KNOWN_HOSTS_TORRENTS }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..d9f985fe6 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint scripts + +on: + workflow_dispatch: + pull_request: + +jobs: + + Shellcheck: + + name: Shell script analysis + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'Armbian' }} + steps: + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Environment variables + run: sudo -E bash -c set + + - name: "Shellcheck lint error report in diff format" + shell: bash {0} + run: | + + for file in $(find . -type f -executable ! -path '*/.git*/*' -exec grep -Iq . {} \; -print); do shellcheck -x $file; done From 95ca137009ac2e07872937b87c56b58aff7c14c4 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 Dec 2023 21:08:49 +0100 Subject: [PATCH 35/48] Update debian.yml --- .github/workflows/debian.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 0a7df186b..1e272d134 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -21,7 +21,7 @@ jobs: homepage: "https://www.armbian.com" section: "default" priority: "optional" - depends: "" + depends: "bash" description: "Configng" secrets: From 88db4691892dea57d387676fa3e43e02fd827821 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 Dec 2023 21:23:44 +0100 Subject: [PATCH 36/48] Remove not needed Action secrets --- .github/workflows/debian.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 1e272d134..1a50f6ae4 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -27,5 +27,3 @@ jobs: secrets: GPG_PRIVATE_KEY: ${{ secrets.GPG_KEY1 }} PASSPHRASE: ${{ secrets.GPG_PASSPHRASE1 }} - SSH_KEY_TORRENTS: ${{ secrets.KEY_TORRENTS }} - KNOWN_HOSTS_UPLOAD: ${{ secrets.KNOWN_HOSTS_TORRENTS }} From 4eb09c44153b3349ef82e5963a9a3728f5078e9c Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Wed, 6 Dec 2023 21:37:33 +0100 Subject: [PATCH 37/48] Mapping for packaging --- debian.conf | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 debian.conf diff --git a/debian.conf b/debian.conf new file mode 100644 index 000000000..0f40161c0 --- /dev/null +++ b/debian.conf @@ -0,0 +1,4 @@ +LICENSE:/usr/share/doc/armbian-configng/ +share:/usr/share/doc/armbian-configng/ +lib:/usr/lib/ +bin:/usr/bin/ From 237df1e5003443be5056f38670f97aa6c4f9ecc6 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 Dec 2023 21:39:38 +0100 Subject: [PATCH 38/48] Update debian.conf --- debian.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/debian.conf b/debian.conf index 0f40161c0..cef41f331 100644 --- a/debian.conf +++ b/debian.conf @@ -1,4 +1,3 @@ -LICENSE:/usr/share/doc/armbian-configng/ share:/usr/share/doc/armbian-configng/ lib:/usr/lib/ bin:/usr/bin/ From c6348a3e087aa3f68cc22dd0e22c2a38b9a0e4c7 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 Dec 2023 21:42:11 +0100 Subject: [PATCH 39/48] Update debian.conf --- debian.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian.conf b/debian.conf index cef41f331..b3aa269fb 100644 --- a/debian.conf +++ b/debian.conf @@ -1,3 +1,3 @@ -share:/usr/share/doc/armbian-configng/ +share:/usr/share/doc lib:/usr/lib/ bin:/usr/bin/ From 6f9b8056d1517eee52fa6ab9ef01263aa7423e4d Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 9 Dec 2023 15:47:12 +0100 Subject: [PATCH 40/48] Adjust quick install guide since we are making a repository on the fly (#21) * Update Quick start After verifying if we put it into wanted folders and if it works, automation builds upon changes. * Update README.md * Update README.md * Updated readme.md function to redirect to /share/doc/README.md * Run doc gen * RM genterated documents folder * bin/armbian-configng --dev --doc codespace * deleted: share/doc/armbian-configng/README.md * Revert "Updated readme.md function to redirect to /share/doc/README.md" This reverts commit db6f4f4034cf2d00361fe0ccdbe4c943b2e3e07d. * modified: debian.conf path --------- Co-authored-by: Joey Turner --- README.md | 11 +++-- debian.conf | 6 +-- .../armbian-configng}/armbian-configng.csv | 1 + .../armbian-configng/armbian-configng.html} | 12 +++++- .../armbian-configng}/armbian-configng.json | 8 ++++ .../armbian-configng/armbianCPU.svg} | 0 share/doc/armbian-configng/index.html | 42 +++++++++++++++++++ 7 files changed, 69 insertions(+), 11 deletions(-) rename share/{armbian-configng/data => doc/armbian-configng}/armbian-configng.csv (82%) rename share/{armbian-configng/index.html => doc/armbian-configng/armbian-configng.html} (88%) rename share/{armbian-configng/data => doc/armbian-configng}/armbian-configng.json (76%) rename share/{armbian-configng/armbian-configng.svg => doc/armbian-configng/armbianCPU.svg} (100%) mode change 100644 => 100755 create mode 100755 share/doc/armbian-configng/index.html diff --git a/README.md b/README.md index 69fb59e52..e771a0d64 100755 --- a/README.md +++ b/README.md @@ -11,17 +11,16 @@ ## Quick start Run the following commands: - sudo apt install git - cd ~/ - git clone https://github.com/armbian/configng.git - cd configng - ./bin/armbian-configng --dev + echo "deb [signed-by=/usr/share/keyrings/armbian.gpg] https://armbian.github.io/configng stable main" \ + | sudo tee /etc/apt/sources.list.d/armbian-development.list > /dev/null + + armbian-configng --dev If all goes well you should see the Text-Based User Inerface (TUI) ### To see a list of all functions and their descriptions, run the following command: ~~~ -bash ~/configng/bin/armbian-configng -h +armbian-configng -h ~~~ ## Coding Style follow the following coding style: diff --git a/debian.conf b/debian.conf index b3aa269fb..465e3080c 100644 --- a/debian.conf +++ b/debian.conf @@ -1,3 +1,3 @@ -share:/usr/share/doc -lib:/usr/lib/ -bin:/usr/bin/ +share:/usr/ +lib:/usr/ +bin:/usr/ diff --git a/share/armbian-configng/data/armbian-configng.csv b/share/doc/armbian-configng/armbian-configng.csv similarity index 82% rename from share/armbian-configng/data/armbian-configng.csv rename to share/doc/armbian-configng/armbian-configng.csv index ee79cbe10..717776e70 100755 --- a/share/armbian-configng/data/armbian-configng.csv +++ b/share/doc/armbian-configng/armbian-configng.csv @@ -2,3 +2,4 @@ Function Name,Group Name,Description,Options,Category,Category Description NMTUI,network,Network Manager.,none.,network,Network Wired wireless Bluetooth access point Hello,system,Hello System.,none,system,System and Security Bencharking,monitor,Armbian Monitor and Bencharking.,,system,System and Security +Install,system,Armbian installer,none.,system,System and Security diff --git a/share/armbian-configng/index.html b/share/doc/armbian-configng/armbian-configng.html similarity index 88% rename from share/armbian-configng/index.html rename to share/doc/armbian-configng/armbian-configng.html index d5e3ccce4..b80abc8df 100755 --- a/share/armbian-configng/index.html +++ b/share/doc/armbian-configng/armbian-configng.html @@ -3,7 +3,7 @@ - Armbian index + Armbian armbian-configng + + + + + + + + + + + + + + + +
Function NameGroup NameDescriptionOptionsCategoryCategory Description
NMTUInetworkNetwork Manager.none.networkNetwork Wired wireless Bluetooth access point
HellosystemHello System.nonesystemSystem and Security
BencharkingmonitorArmbian Monitor and Bencharking.systemSystem and Security
InstallsystemArmbian installernone.systemSystem and Security
+ + + From ea81912f58aa59bd94135d2ee8fdb6965def8e71 Mon Sep 17 00:00:00 2001 From: Joey Turner Date: Sat, 9 Dec 2023 07:48:21 -0700 Subject: [PATCH 41/48] edit deb.conf (#23) Co-authored-by: Igor From cfd94f2fb2225ad623573afb649a465546cba63c Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 9 Dec 2023 15:48:47 +0100 Subject: [PATCH 42/48] Lint only bash files (#24) * Lint only bash files * Update lint.yml --- .github/workflows/lint.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d9f985fe6..047bb3b08 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,4 +23,9 @@ jobs: shell: bash {0} run: | - for file in $(find . -type f -executable ! -path '*/.git*/*' -exec grep -Iq . {} \; -print); do shellcheck -x $file; done + for file in $(find . -type f -executable ! -path '*/.git*/*' -exec grep -Iq . {} \; -print); do + if grep -qE "^#\!/.*bash" $file; then + shellcheck --severity=error $file || ret=$? + fi + done + exit $ret From 0e735bed1d938a6a78a19709d5b95c9860f902fa Mon Sep 17 00:00:00 2001 From: Joey Turner Date: Sun, 10 Dec 2023 08:58:43 +0000 Subject: [PATCH 43/48] edit for shellcheck error --- bin/armbian-interface | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/armbian-interface b/bin/armbian-interface index ff095fdc0..5f6e27c12 100755 --- a/bin/armbian-interface +++ b/bin/armbian-interface @@ -36,7 +36,7 @@ # ## DIRECTORY variable to the absolute path of the script's directory -directory="$(dirname "$(readlink -f "$0")")" +# directory="$(dirname "$(readlink -f "$0")")" filename=$(basename "${BASH_SOURCE[0]}") ## DIALOG variable to the absolute path of the script's directory @@ -70,7 +70,7 @@ show_message(){ # Display the "OK" message box with the input data [[ $DIALOG != "bash" ]] && $DIALOG --title "Message Box" --msgbox "$input" 0 0 [[ $DIALOG == "bash" ]] && echo -e "$input" - [[ $DIALOG == "bash" ]] && read -p "Press [Enter] to continue..." ; echo "" ; exit 1 + [[ $DIALOG == "bash" ]] && read -p -r "Press [Enter] to continue..." ; echo "" ; exit 1 } @@ -116,4 +116,4 @@ show_menu(){ [[ $1 == "-o" ]] && show_message ; [[ $1 == "-h" ]] && show_help ; [[ $1 == "-p" ]] && show_popup ; -[[ -z "$@" ]] && show_help ; +[[ -z "$*" ]] && show_help ; From a43f3887f03717fe1c5f46071f89c3fdb325a450 Mon Sep 17 00:00:00 2001 From: schwar3kat <61094841+schwar3kat@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:29:47 +1300 Subject: [PATCH 44/48] Fix_typo's --- README.md | 6 +++--- lib/armbian-configng/system/see_monitor.sh | 4 ++-- share/doc/armbian-configng/armbian-configng.csv | 2 +- share/doc/armbian-configng/armbian-configng.html | 4 ++-- share/doc/armbian-configng/armbian-configng.json | 4 ++-- share/doc/armbian-configng/index.html | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e771a0d64..b3e4cb1d5 100755 --- a/README.md +++ b/README.md @@ -72,12 +72,12 @@ Network Wired wireless Bluetooth access point ### see_monitor.sh - **Group Name:** monitor - - **Action Name:** Bencharking + - **Action Name:** Benchmarking - **Options:** - - **Description:** Armbian Monitor and Bencharking. + - **Description:** Armbian Monitor and Benchmarking. -# Inclueded projects +# Included projects - [Bash Utility](https://labbots.github.io/bash-utility) - [Armbian config](https://github.com/armbian/config.git) diff --git a/lib/armbian-configng/system/see_monitor.sh b/lib/armbian-configng/system/see_monitor.sh index da1ef94d7..5d7ed760a 100644 --- a/lib/armbian-configng/system/see_monitor.sh +++ b/lib/armbian-configng/system/see_monitor.sh @@ -7,12 +7,12 @@ # warranty of any kind, whether express or implied. -# @description Armbian Monitor and Bencharking. +# @description Armbian Monitor and Benchmarking. # # @exitcode 0 If successful. # # @options none -function monitor::Bencharking(){ +function monitor::Benchmarking(){ see_menu #| armbian-interface -o return 0 ; } diff --git a/share/doc/armbian-configng/armbian-configng.csv b/share/doc/armbian-configng/armbian-configng.csv index 717776e70..40dc84ad9 100755 --- a/share/doc/armbian-configng/armbian-configng.csv +++ b/share/doc/armbian-configng/armbian-configng.csv @@ -1,5 +1,5 @@ Function Name,Group Name,Description,Options,Category,Category Description NMTUI,network,Network Manager.,none.,network,Network Wired wireless Bluetooth access point Hello,system,Hello System.,none,system,System and Security -Bencharking,monitor,Armbian Monitor and Bencharking.,,system,System and Security +Bencharking,monitor,Armbian Monitor and Benchmarking.,,system,System and Security Install,system,Armbian installer,none.,system,System and Security diff --git a/share/doc/armbian-configng/armbian-configng.html b/share/doc/armbian-configng/armbian-configng.html index b80abc8df..40c3d7804 100755 --- a/share/doc/armbian-configng/armbian-configng.html +++ b/share/doc/armbian-configng/armbian-configng.html @@ -63,9 +63,9 @@

armbian-configng

"Category Description": "System and Security" }, { - "Function Name": "Bencharking", + "Function Name": "Benchmarking", "Group Name": "monitor", - "Description": "Armbian Monitor and Bencharking.", + "Description": "Armbian Monitor and Benchmarking.", "Options": "", "Category": "system", "Category Description": "System and Security" diff --git a/share/doc/armbian-configng/armbian-configng.json b/share/doc/armbian-configng/armbian-configng.json index eb936b4bf..806c67fa9 100755 --- a/share/doc/armbian-configng/armbian-configng.json +++ b/share/doc/armbian-configng/armbian-configng.json @@ -16,9 +16,9 @@ "Category Description": "System and Security" }, { - "Function Name": "Bencharking", + "Function Name": "Benchmarking", "Group Name": "monitor", - "Description": "Armbian Monitor and Bencharking.", + "Description": "Armbian Monitor and Benchmarking.", "Options": "", "Category": "system", "Category Description": "System and Security" diff --git a/share/doc/armbian-configng/index.html b/share/doc/armbian-configng/index.html index 0f3d2f124..dc65e1110 100755 --- a/share/doc/armbian-configng/index.html +++ b/share/doc/armbian-configng/index.html @@ -34,7 +34,7 @@ Category Description - NMTUInetworkNetwork Manager.none.networkNetwork Wired wireless Bluetooth access pointHellosystemHello System.nonesystemSystem and SecurityBencharkingmonitorArmbian Monitor and Bencharking.systemSystem and SecurityInstallsystemArmbian installernone.systemSystem and Security + NMTUInetworkNetwork Manager.none.networkNetwork Wired wireless Bluetooth access pointHellosystemHello System.nonesystemSystem and SecurityBenchmarkingmonitorArmbian Monitor and Benchmarking.systemSystem and SecurityInstallsystemArmbian installernone.systemSystem and Security From 74fa1f41217cfb35b5222ec45e7fa7bc80f5e5c9 Mon Sep 17 00:00:00 2001 From: Tearran Date: Fri, 12 Apr 2024 00:19:15 -0700 Subject: [PATCH 45/48] Runtime & documentation generation. --- .github/workflows/ftp-deploy.yml.hold | 34 + README.md | 0 bin/README.md | 90 +++ bin/configng | 78 ++ lib/armbian-configng/config.ng.docs.sh | 410 ++++++++++ lib/armbian-configng/config.ng.functions.sh | 739 ++++++++++++++++++ lib/armbian-configng/config.ng.jobs.json | 295 +++++++ lib/armbian-configng/config.ng.network.sh | 108 +++ lib/armbian-configng/config.ng.runtime.dev.sh | 45 ++ lib/armbian-configng/config.ng.runtime.sh | 60 ++ share/doc/armbian-configng/Functions.md | 35 + share/doc/armbian-configng/Home.md | 90 +++ share/doc/armbian-configng/Menu.md | 233 ++++++ share/doc/armbian-configng/ScreenShot.md | 17 + share/doc/armbian-configng/Standards.md | 37 + 15 files changed, 2271 insertions(+) create mode 100644 .github/workflows/ftp-deploy.yml.hold mode change 100755 => 100644 README.md create mode 100644 bin/README.md create mode 100644 bin/configng create mode 100644 lib/armbian-configng/config.ng.docs.sh create mode 100644 lib/armbian-configng/config.ng.functions.sh create mode 100644 lib/armbian-configng/config.ng.jobs.json create mode 100644 lib/armbian-configng/config.ng.network.sh create mode 100644 lib/armbian-configng/config.ng.runtime.dev.sh create mode 100644 lib/armbian-configng/config.ng.runtime.sh create mode 100644 share/doc/armbian-configng/Functions.md create mode 100644 share/doc/armbian-configng/Home.md create mode 100644 share/doc/armbian-configng/Menu.md create mode 100644 share/doc/armbian-configng/ScreenShot.md create mode 100644 share/doc/armbian-configng/Standards.md diff --git a/.github/workflows/ftp-deploy.yml.hold b/.github/workflows/ftp-deploy.yml.hold new file mode 100644 index 000000000..1a956dcdd --- /dev/null +++ b/.github/workflows/ftp-deploy.yml.hold @@ -0,0 +1,34 @@ +on: push +name: 🚀 Deploy website on push + +jobs: + web-deploy: + name: 🎉 Deploy + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'Tearran' }} + steps: + - name: 🚚 Get latest code + uses: actions/checkout@v3 + + - name: Install ShellCheck + run: sudo apt-get install pandoc git shellcheck + - name: Run ShellCheck + run: | + ret=0 + for file in $(find . -type f -name "*.sh"); do + shellcheck --severity=error $file || ret=$? + done + exit $ret + + - name: Generate website + run: ./bin/armbian-configng --dev --web + + - name: 📂 Sync files + uses: SamKirkland/FTP-Deploy-Action@v4.3.4 + with: + server: ${{ secrets.SERVER_HOST}} # replace with your server + username: ${{ secrets.SERVER_USERNAME }} + password: ${{ secrets.SERVER_PASSWORD }} + server-dir: / # replace with your server directory + local-dir: share/armbian-configng/web/ # replace with your local directory + diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 000000000..1e772cd04 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,90 @@ + +# Armbian configuration utility +Utility for configuring your board, divided into four main sections: + +- System - system and security settings, +- Network - wired, wireless, Bluetooth, access point, +- Personal - timezone, language, hostname, +- Software - system and 3rd party software install. + + + +To Configure and change global sytem settings, run the following command: `./armbian-configng` + +*** + +Following was updated on: +Thu Apr 11 02:23:43 AM MST 2024. + +*** +- ## **System** + - **S01** - Description: Enable Armbina kernal upgrades + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s01) + - **S02** - Description: Disable Armbina kernal upgrades + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s02) + - **S03** - Description: Edit the boot enviroment (WIP) + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s03) + + +- ## **Network** + - **BT0** - Description: Install Bluetooth support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt0) + - **BT1** - Description: Remove Bluetooth support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt1) + - **BT3** - Description: Bluetooth Discover + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt3) + - **IR0** - Description: Install Infrared support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#ir0) + - **IR1** - Description: Uninstall Infrared support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#ir1) + - **N00** - Description: Manage wifi network connections + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n00) + - **N01** - Description: Advanced Edit /etc/network/interface + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n01) + - **N02** - Description: Disconect and forget all wifi connections (Advanced) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n02) + - **N03** - Description: Toggle system IPv6/IPv4 internet protical + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n03) + + +- ## **Localisation** + - **L00** - Description: Change Globla timezone (WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l00) + - **L01** - Description: Change Locales reconfigure the language and charitorset + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l01) + - **L02** - Description: Change Keyboard layout + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l02) + - **L03** - Description: Change APT mirrors + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l03) + + +- ## **Software** + - **I00** - Description: Update Application Repository + - Status: [review](https://github.com/armbian/configng/wiki/Menu#i00) + - **I01** - Description: CLI System Monitor + - Status: [review](https://github.com/armbian/configng/wiki/Menu#i01) + + +- ## **Help** + - **H00** - Description: About This systme. (WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#h00) + - **H02** - Description: List of Config function(WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#h02) + + +*** + +# Development +get the lastet version of the utility by running the following command: + +~~~ +git clone https://github.com/armbian/configng.git +cd configng +~~~ + + + +## Note: +> +> The Bash procedures embedded within the JSON structure are meticulously designed with a focus on clear naming conventions and the simplicity of key pairs. These procedures serve multiple purposes, including facilitating the generation of content in various formats, such as Whiptail, Markdown, json out and others. Moreover, they are utilized for evaluation and execution of commands outlined in the JSON structure. +> diff --git a/bin/configng b/bin/configng new file mode 100644 index 000000000..1ae72391c --- /dev/null +++ b/bin/configng @@ -0,0 +1,78 @@ +#!/bin/bash + + +tput init +# +# Language-based variable assignment for script directory path +# This serves as a Rosetta Stone for developers, +# allowing them to use the variable name they are most comfortable with. + +# allows CTRL c to exit +trap "exit" INT TERM + +# Get the script directory +script_dir="$(dirname "$0")" + +# Define the lib directory one level up from the script directory +lib_dir="$script_dir/../lib/armbian-configng" +doc_dir="$script_dir/../share/doc/armbian-configng" +# Check for the existence of the config.ng.jobs.json file in the lib directory +json_file="$lib_dir/config.ng.jobs.json" + +# +# Load The Bash procedure Objects +json_data=$(cat "$json_file") + + +# +# We're checking if the 'whiptail' command is available on the system. +# 'whiptail' is a simple dialog box utility that works well with Bash. It doesn't have all the features of some other dialog box utilities, but it does everything we need for this script. +# If 'whiptail' is available, we assign its name to the variable DIALOG. This way, we can easily switch to another dialog box utility in the future if we need to, just by changing this one line of code. +[[ -x "$(command -v whiptail)" ]] && DIALOG="whiptail" + + +# +# Prepare the module options array +declare -A module_options + + +# +# Load configng core functions and module options array + + +source "$lib_dir/config.ng.functions.sh" +set_runtime_variables +echo "Loaded Runtime variables..." | show_infobox ; +set_newt_colors 2 +echo "Loaded Dialog..." | show_infobox ; +source "$lib_dir/config.ng.docs.sh" +echo "Loaded Docs..." | show_infobox ; +source "$lib_dir/config.ng.network.sh" +echo "Loaded Network helpers..." | show_infobox ; + + +# +# Loads the varibles from beta armbian-config for runtime handeling + +source "$lib_dir/config.ng.runtime.sh" ; +echo "Loaded Runtime conditions..." | show_infobox ; + +# +# if not sudo +# Runtime "include this script" for USER and development setup condistion +if [[ $EUID != 0 ]]; then + source "$lib_dir/config.ng.runtime.dev.sh" ; + echo "Loaded Develoment +Runtime conditions..." | show_infobox ; + +fi + +tput clear +# +# Generate the top menu with the modified Object data +while generate_top_menu "$json_data"; do tput clear ; done + + +# +# Exit the script with a success status code +exit 0 diff --git a/lib/armbian-configng/config.ng.docs.sh b/lib/armbian-configng/config.ng.docs.sh new file mode 100644 index 000000000..104900d71 --- /dev/null +++ b/lib/armbian-configng/config.ng.docs.sh @@ -0,0 +1,410 @@ +#!/bin/bash + +# This file is part of Armbian configuration utility. + +module_options+=( + ["generate_readme,author"]="Joey Turner" + ["generate_readme,ref_link"]="https://github.com/armbian/configng/blob/main/lib/armbian-configng/documents.sh#L18" + ["generate_readme,feature"]="generate_readme" + ["generate_readme,desc"]="Generate Document files." + ["generate_readme,example"]="generate_readme" + ["generate_readme,status"]="review" + ["generate_readme,doc_link"]="" +) +# +# Function to generate the README.md file +# +function generate_readme() { + + # Get the current date + local current_date=$(date) + +# [[ ! -d "$script_dir/images" ]] && mkdir -p "$script_dir/images" && generate_svg > "$script_dir/images/logo.svg" ; +# [[ ! -f "$script_dir/images/logo.svg" ]] && generate_svg > "$script_dir/images/logo.svg" ; + + + +echo "$(see_jobs_list)" > "$script_dir/README.md" + + + + echo "Documents have been updated." | show_infobox + + +###################################### + +echo "Updating WIKI Menu" | show_infobox +cp "$script_dir/README.md" "$doc_dir/Home.md" + + +###################################### + +echo "Updating WIKI Functions" | show_infobox +cat << EOF > "$doc_dir/Functions.md" + +# Helper functions +A list of the heper function ie bash prosedures used in Jobs file. + +$(see_function_table_md) + +EOF + +###################################### + +echo "Updating WIKI HowTo" | show_infobox +cat << EOF > "$doc_dir/Menu.md" + +# Armbian-config Menu list. +armbian-config jobs list. + +$(see_jq_menu_list) + +EOF + +###################################### + +# show_infobox <<< $( echo "$(generate_json_options)" > "$script_dir/docs/config-helpers.json" ) +# Print a message indicating that README.md has been updated +# echo "Documents have been updated." | show_infobox + +} + + +module_options+=( + ["serve_doc,author"]="Tearran" + ["serve_doc,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L89" + ["serve_doc,feature"]="serve_doc" + ["serve_doc,desc"]="Serve the edit and debug server." + ["serve_doc,example"]="serve_doc" + ["serve_doc,status"]="review" + ["serve_doc,doc_link"]="" +) +# +# Function to serve the edit and debug server +# +function serve_doc() { + if [[ "$(id -u)" == "0" ]] ; then + echo "Red alert! not for sude user" + exit 1 + fi + if [[ -z $CODESPACES ]]; then + # Start the Python server in the background + python3 -m http.server > /tmp/config.log 2>&1 & + local server_pid=$! + local input=(" + Starting server... + Server PID: $server_pid + + Press [Enter] to exit" + ) + + $DIALOG --title "Message Box" --msgbox "$input" 0 0 + + # Stop the server + kill "$server_pid" + else + echo "Info:GitHub Codespace" + exit 0 + fi +} + + +module_options+=( + ["see_use,author"]="Tearran" + ["see_use,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L126" + ["see_use,feature"]="see_use" + ["see_use,desc"]="Show the usage of the functions." + ["see_use,example"]="see_use" + ["see_use,status"]="review" + ["see_use,doc_link"]="" +) +# +# Function to parse the key-pairs (WIP) +# +function see_use() { + mod_message="Usage: \n\n" + # Iterate over the options + for key in "${!module_options[@]}"; do + # Split the key into function_name and type + IFS=',' read -r function_name type <<< "$key" + # If the type is 'long', append the option to the help message + if [[ "$type" == "feature" ]]; then + mod_message+="${module_options["$function_name,feature"]} - ${module_options["$function_name,desc"]}\n" + mod_message+=" ${module_options["$function_name,example"]}\n\n" + fi + done + + echo -e "$mod_message" +} + + +module_options+=( + ["generate_json_options,author"]="Tearran" + ["generate_json_options,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L149" + ["generate_json_options,feature"]="generate_json" + ["generate_json_options,desc"]="Generate JSON-like object file." + ["generate_json_options,example"]="generate_json" + ["generate_json_options,status"]="review" + ["generate_json_options,doc_link"]="" +) +# +# Function to generate a JSON-like object file +# +function generate_json_options() { + echo -e "{\n\"configng-helpers\" : [" +features=() +for key in "${!module_options[@]}"; do + if [[ $key == *",feature" ]]; then + features+=("${module_options[$key]}") + fi +done + +for index in "${!features[@]}"; do + feature=${features[$index]} + desc_key="${feature},desc" + example_key="${feature},example" + author_key="${feature},author" + ref_key="${feature},ref_link" + status_key="${feature},status" + doc_key="${feature},doc_link" + author="${module_options[$author_key]}" + ref_link="${module_options[$ref_key]}" + status="${module_options[$status_key]}" + doc_link="${module_options[$doc_key]}" + desc="${module_options[$desc_key]}" + example="${module_options[$example_key]}" + echo " {" + echo " \"id\": \"$feature\"," + echo " \"Author\": \"$author\"," + echo " \"src_reference\": \"$ref_link\"," + echo " \"description\": \"$desc\"," + echo " \"command\": [ \"$example\" ]", + echo " \"status\": \"$status\"," + echo " \"doc_link\": \"$doc_link\"" + if [ $index -ne $(( ${#features[@]}-1 )) ]; then + echo " }," + else + echo " }" + fi +done +echo "]" +echo "}" +} + + +module_options+=( + ["generate_svg,author"]="Tearran" + ["generate_svg,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#201" + ["generate_svg,feature"]="generate_svg" + ["generate_svg,desc"]="Generate 'Armbian CPU logo' SVG for docunment file." + ["generate_svg,example"]="generate_svg" + ["generate_svg,status"]="review" + ["generate_svg,doc_link"]="" +) +# +# This function is used to generate a armbian CPU logo +# +function generate_svg(){ + +cat << EOF + + + + + + +EOF +} + + +module_options+=( + ["generate_jobs_from_json,author"]="Tearran" + ["generate_jobs_from_json,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L223" + ["generate_jobs_from_json,feature"]="generate_jobs_from_json" + ["generate_jobs_from_json,desc"]="Generate jobs from JSON file." + ["generate_jobs_from_json,example"]="generate_jobs_from_json" + ["generate_jobs_from_json,status"]="review" + ["generate_jobs_from_json,doc_link"]="" +) +# +# This function is used to generate jobs links Table from JSON file. +# +function see_jobs_from_json_md() { + +echo -e "\n" + +# Use jq to parse the JSON +menu_items=$(jq -r '.menu | length' "$json_file") + +for (( i=0; i<$menu_items; i++ )) +do + cat=$(jq -r ".menu[$i].id" "$json_file") + description=$(jq -r ".menu[$i].description" "$json_file") + #echo -e "## $cat\n" + #echo -e "$description\n" + echo -e "| "$cat" | ID | Description | Documents | Status |" + echo -e "|:------ | :-- | :---------- | --------: | ------:|" + + sub_items=$(jq -r ".menu[$i].sub | length" "$json_file") + + for (( j=0; j<$sub_items; j++ )) + do + id=$(jq -r ".menu[$i].sub[$j].id" "$json_file") + id_link=$(jq -r ".menu[$i].sub[$j].id" "$json_file"| tr '[:upper:]' '[:lower:]') + description=$(jq -r ".menu[$i].sub[$j].description" "$json_file") + command=$(jq -r ".menu[$i].sub[$j].command" "$json_file") + status=$(jq -r ".menu[$i].sub[$j].status" "$json_file") + doc_link=$(jq -r ".menu[$i].sub[$j].doc_link" "$json_file") + + # Check if src_reference and doc_link are null + if [ "$doc_link" == "" ]; then doc_link="https://github.com/armbian/configng/wiki/Menu#$id_link"; else doc_link="[Document]($doc_link)"; fi + + echo -e "| | $id | $description | $doc_link | $status |" + + done + echo -e "\n" +done + +} + +function see_jobs_list() { + + cat << EOF + +# Armbian configuration utility +Utility for configuring your board, divided into four main sections: + +- System - system and security settings, +- Network - wired, wireless, Bluetooth, access point, +- Personal - timezone, language, hostname, +- Software - system and 3rd party software install. + + + +To Configure and change global sytem settings, run the following command: \`./armbian-configng\` + +*** + +Following was updated on: +$current_date. + +*** +EOF + + # Use jq to parse the JSON + menu_items=$(jq -r '.menu | length' "$json_file") + + for (( i=0; i<$menu_items; i++ )) + do + cat=$(jq -r ".menu[$i].id" "$json_file") + description=$(jq -r ".menu[$i].description" "$json_file") + + echo -e "- ## **$cat** " + #echo "$description" + + sub_items=$(jq -r ".menu[$i].sub | length" "$json_file") + + for (( j=0; j<$sub_items; j++ )) + do + id=$(jq -r ".menu[$i].sub[$j].id" "$json_file") + id_link=$(jq -r ".menu[$i].sub[$j].id" "$json_file"| tr '[:upper:]' '[:lower:]') + description=$(jq -r ".menu[$i].sub[$j].description" "$json_file") + command=$(jq -r ".menu[$i].sub[$j].command" "$json_file") + status=$(jq -r ".menu[$i].sub[$j].status" "$json_file") + doc_link=$(jq -r ".menu[$i].sub[$j].doc_link" "$json_file") + + # Check if src_reference and doc_link are null + if [ "$doc_link" == "" ]; then + doc_link="https://github.com/armbian/configng/wiki/Menu#$id_link"; + else doc_link="$doc_link"; + fi + + echo -e " - **$id** - Description: $description" + echo -e " - Status: [$status]($doc_link)" + + done + echo -e "\n" + done + +cat << EOF +*** + +# Development +get the lastet version of the utility by running the following command: + +~~~ +git clone https://github.com/armbian/configng.git +cd configng +~~~ + + + +## Note: +> +> The Bash procedures embedded within the JSON structure are meticulously designed with a focus on clear naming conventions and the simplicity of key pairs. These procedures serve multiple purposes, including facilitating the generation of content in various formats, such as Whiptail, Markdown, json out and others. Moreover, they are utilized for evaluation and execution of commands outlined in the JSON structure. +> + +EOF + +} + + +module_options+=( + ["see_function_table_md,author"]="Tearran" + ["see_function_table_md,ref_link"]="" + ["see_function_table_md,feature"]="see_function_table_md" + ["see_function_table_md,desc"]="Generate this markdown table of all module_options" + ["see_function_table_md,example"]="see_function_table_md" + ["see_function_table_md,status"]="review" + ["see_function_table_md,doc_link"]="" +) +# +# This function is used to generate a markdown table from the module_options array +# +function see_function_table_md() { + mod_message="| Feature | Description | Credit | Reference | Documents | Status |\n" + mod_message+="|:------- | ----------- | ----------- |:---------:|:---------:|:------:|\n" + # Iterate over the options + for key in "${!module_options[@]}"; do + # Split the key into function_name and type + IFS=',' read -r function_name type <<< "$key" + # If the type is 'feature', append the option to the help message + if [[ "$type" == "feature" ]]; then + status=${module_options["$function_name,status"]} + ref_link=${module_options["$function_name,ref_link"]} + doc_link=${module_options["$function_name,doc_link"]} + ref_link_md=$([[ -n "$ref_link" ]] && echo "[Source]($ref_link)" || echo "X") + doc_link_md=$([[ -n "$doc_link" ]] && echo "[Document]($doc_link)" || echo "X") + status_md=$([[ -z "$ref_link" ]] && echo "source link Needed" || ([[ ( -n "$ref_link" && -n "$doc_link") ]] && echo "Review" || echo "$status" ) ) + mod_message+="| ${module_options["$function_name,feature"]} | ${module_options["$function_name,desc"]} | ${module_options["$function_name,author"]} | $ref_link_md | $doc_link_md | $status_md | \n" + fi + done + + echo -e "$mod_message" +} + + +module_options+=( + ["see_jq_menu_list,author"]="Tearran" + ["see_jq_menu_list,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L304" + ["see_jq_menu_list,feature"]="see_jq_menu_list" + ["see_jq_menu_list,desc"]="Generate a markdown list json objects using jq." + ["see_jq_menu_list,example"]="see_jq_menu_list" + ["see_jq_menu_list,status"]="review" + ["see_jq_menu_list,doc_link"]="" +) +# +# This function is used to generate a markdown list from the json object using jq. +# +function see_jq_menu_list() { + +jq -r ' + .menu[] | + .sub[] | + "### " + .id + "\n\n" + + .description + "\n\nJobs:\n\n~~~\n" + + (.command | join("\n")) + + "\n~~~\n" +' $json_file +} + diff --git a/lib/armbian-configng/config.ng.functions.sh b/lib/armbian-configng/config.ng.functions.sh new file mode 100644 index 000000000..9fe363d4b --- /dev/null +++ b/lib/armbian-configng/config.ng.functions.sh @@ -0,0 +1,739 @@ +#!/bin/bash + + + + + +module_options+=( +["check_desktop,author"]="Igor Pecovnik" +["check_desktop,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L16" +["check_desktop,feature"]="check_desktop" +["check_desktop,desc"]="Migrated procedures from Armbian config." +["check_desktop,example"]="check_desktop" +["check_desktop,status"]="review" +["check_desktop,doc_link"]="" +) +# +# read desktop parameters +# +function check_desktop() { + + DISPLAY_MANAGER=""; DESKTOP_INSTALLED="" + check_if_installed nodm && DESKTOP_INSTALLED="nodm"; + check_if_installed lightdm && DESKTOP_INSTALLED="lightdm"; + check_if_installed lightdm && DESKTOP_INSTALLED="gnome"; + [[ -n $(service lightdm status 2> /dev/null | grep -w active) ]] && DISPLAY_MANAGER="lightdm" + [[ -n $(service nodm status 2> /dev/null | grep -w active) ]] && DISPLAY_MANAGER="nodm" + [[ -n $(service gdm status 2> /dev/null | grep -w active) ]] && DISPLAY_MANAGER="gdm" + +} + + + +menu_options+=( +["get_headers_kernel,author"]="Igor Pecovnik" +["get_headers_kernel,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L39" +["get_headers_kernel,feature"]="get_headers_kernel" +["get_headers_kernel,desc"]="Migrated procedures from Armbian config." +["get_headers_kernel,example"]="get_headers_kernel" +["get_headers_kernel,status"]="review" +["get_headers_kernel,doc_link"]="" +) +# +# install kernel headers +# +function get_headers_install() { + + if [[ -f /etc/armbian-release ]]; then + INSTALL_PKG="linux-headers-${BRANCH}-${LINUXFAMILY}"; + else + INSTALL_PKG="linux-headers-$(uname -r | sed 's/'-$(dpkg --print-architecture)'//')"; + fi + + debconf-apt-progress -- apt-get -y install ${INSTALL_PKG} || exit 1 + +} + +module_options+=( +["set_header_remove,author"]="Igor Pecovnik" +["set_header_remove,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L64" +["set_header_remove,feature"]="set_header_remove" +["set_header_remove,desc"]="Migrated procedures from Armbian config." +["set_header_remove,example"]="set_header_remove" +["set_header_remove,doc_link"]="" +["set_header_remove,status"]="review" +["set_header_remove,doc_ink"]="" +) +# +# remove kernel headers +# +function set_header_remove() { + + REMOVE_PKG="linux-headers-*" + if [[ -n $(dpkg -l | grep linux-headers) ]]; then + debconf-apt-progress -- apt-get -y purge ${REMOVE_PKG} + rm -rf /usr/src/linux-headers* + else + debconf-apt-progress -- apt-get -y install ${INSTALL_PKG} + fi + # cleanup + apt clean + debconf-apt-progress -- apt -y autoremove + +} + + +module_options+=( +["check_if_installed,author"]="Igor Pecovnik" +["check_if_installed,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L88" +["check_if_installed,feature"]="check_if_installed" +["check_if_installed,desc"]="Migrated procedures from Armbian config." +["check_if_installed,example"]="check_if_installed nano" +["check_if_installed,status"]="review" +) +# +# check dpkg status of $1 -- currently only 'not installed at all' case caught +# +function check_if_installed (){ + + local DPKG_Status="$(dpkg -s "$1" 2>/dev/null | awk -F": " '/^Status/ {print $2}')" + if [[ "X${DPKG_Status}" = "X" || "${DPKG_Status}" = *deinstall* ]]; then + return 1 + else + return 0 + fi + +} + + +module_options+=( +["is_package_manager_running,author"]="Igor Pecovnik" +["is_package_manager_running,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L111" +["is_package_manager_running,feature"]="is_package_manager_running" +["is_package_manager_running,desc"]="Migrated procedures from Armbian config." +["is_package_manager_running,example"]="is_package_manager_running" +["is_package_manager_running,status"]="review" +) +# +# check if package manager is doing something +# +function is_package_manager_running() { + + if ps -C apt-get,apt,dpkg >/dev/null ; then + [[ -z $scripted ]] && echo -e "\nPackage manager is running in the background. \n\nCan't install dependencies. Try again later." | show_infobox + return 0 + else + return 1 + fi + +} + + +module_options+=( +["set_runtime_variables,author"]="Igor Pecovnik" +["set_runtime_variables,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L136" +["set_runtime_variables,feature"]="set_runtime_variables" +["set_runtime_variables,desc"]="Run time varibales Migrated procedures from Armbian config." +["set_runtime_variables,example"]="set_runtime_variables" +["set_runtime_variables,status"]="review" +) +# +# gather info about the board and start with loading menu variables +# +function set_runtime_variables(){ + + [[ -z "$DIALOG" ]] && echo "Please install whiptail" && exit 1 ; + + DIALOG_CANCEL=1 + DIALOG_ESC=255 + + # we have our own lsb_release which does not use Python. Others shell install it here + if [[ ! -f /usr/bin/lsb_release ]]; then + if is_package_manager_running; then + sleep 3 + fi + debconf-apt-progress -- apt-get update + debconf-apt-progress -- apt -y -qq --allow-downgrades --no-install-recommends install lsb-release + fi + + + + [[ -f /etc/armbian-release ]] && source /etc/armbian-release && ARMBIAN="Armbian $VERSION $IMAGE_TYPE"; + DISTRO=$(lsb_release -is) + DISTROID=$(lsb_release -sc) + KERNELID=$(uname -r) + [[ -z "${ARMBIAN// }" ]] && ARMBIAN="$DISTRO $DISTROID" + DEFAULT_ADAPTER=$(ip -4 route ls | grep default | tail -1 | grep -Po '(?<=dev )(\S+)') + LOCALIPADD=$(ip -4 addr show dev $DEFAULT_ADAPTER | awk '/inet/ {print $2}' | cut -d'/' -f1) + BACKTITLE="Configuration utility, $ARMBIAN" + [[ -n "$LOCALIPADD" ]] && BACKTITLE=$BACKTITLE", "$LOCALIPADD + TITLE="$BOARD_NAME " + [[ -z "${DEFAULT_ADAPTER// }" ]] && DEFAULT_ADAPTER="lo" + OVERLAYDIR="/boot/dtb/overlay"; + [[ "$LINUXFAMILY" == "sunxi64" ]] && OVERLAYDIR="/boot/dtb/allwinner/overlay"; + [[ "$LINUXFAMILY" == "meson64" ]] && OVERLAYDIR="/boot/dtb/amlogic/overlay"; + [[ "$LINUXFAMILY" == "rockchip64" || "$LINUXFAMILY" == "rk3399" || "$LINUXFAMILY" == "rockchip-rk3588" || "$LINUXFAMILY" == "rk35xx" ]] && OVERLAYDIR="/boot/dtb/rockchip/overlay"; + # detect desktop + check_desktop + +} + + +module_options+=( +["set_safe_boot,author"]="Igor Pecovnik" +["set_safe_boot,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L188" +["set_safe_boot,feature"]="set_safe_boot" +["set_safe_boot,desc"]="Freeze/unhold Migrated procedures from Armbian config." +["set_safe_boot,example"]="set_safe_boot unhold or set_safe_boot freeze" +["set_safe_boot,status"]="review" +) +# +# freeze/unhold packages +# +set_safe_boot() { + + check_if_installed linux-u-boot-${BOARD}-${BRANCH} && PACKAGE_LIST+=" linux-u-boot-${BOARD}-${BRANCH}" + check_if_installed linux-image-${BRANCH}-${LINUXFAMILY} && PACKAGE_LIST+=" linux-image-${BRANCH}-${LINUXFAMILY}" + check_if_installed linux-dtb-${BRANCH}-${LINUXFAMILY} && PACKAGE_LIST+=" linux-dtb-${BRANCH}-${LINUXFAMILY}" + check_if_installed linux-headers-${BRANCH}-${LINUXFAMILY} && PACKAGE_LIST+=" linux-headers-${BRANCH}-${LINUXFAMILY}" + + # new BSP + check_if_installed armbian-${LINUXFAMILY} && PACKAGE_LIST+=" armbian-${LINUXFAMILY}" + check_if_installed armbian-${BOARD} && PACKAGE_LIST+=" armbian-${BOARD}" + check_if_installed armbian-${DISTROID} && PACKAGE_LIST+=" armbian-${DISTROID}" + check_if_installed armbian-bsp-cli-${BOARD} && PACKAGE_LIST+=" armbian-bsp-cli-${BOARD}" + check_if_installed armbian-${DISTROID}-desktop-xfce && PACKAGE_LIST+=" armbian-${DISTROID}-desktop-xfce" + check_if_installed armbian-firmware && PACKAGE_LIST+=" armbian-firmware" + check_if_installed armbian-firmware-full && PACKAGE_LIST+=" armbian-firmware-full" + IFS=" " + [[ "$1" == "unhold" ]] && local command="apt-mark unhold" && for word in $PACKAGE_LIST; do $command $word; done | show_infobox + + [[ "$1" == "freeze" ]] && local command="apt-mark hold" && for word in $PACKAGE_LIST; do $command $word; done | show_infobox + +} + + + +module_options+=( +["connect_bt_interface,author"]="Igor Pecovnik" +["connect_bt_interface,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L221" +["connect_bt_interface,feature"]="connect_bt_interface" +["connect_bt_interface,desc"]="Migrated procedures from Armbian config." +["connect_bt_interface,example"]="connect_bt_interface" +["connect_bt_interface,status"]="review" +) +# +# connect to bluetooth device +# +function connect_bt_interface(){ + + IFS=$'\r\n' + GLOBIGNORE='*' + show_infobox <<< "\nDiscovering Bluetooth devices ... " + BT_INTERFACES=($(hcitool scan | sed '1d')) + + local LIST=() + for i in "${BT_INTERFACES[@]}" + do + local a=$(echo ${i[0]//[[:blank:]]/} | sed -e 's/^\(.\{17\}\).*/\1/') + local b=${i[0]//$a/} + local b=$(echo $b | sed -e 's/^[ \t]*//') + LIST+=( "$a" "$b") + done + + LIST_LENGTH=$((${#LIST[@]}/2)); + if [ "$LIST_LENGTH" -eq 0 ]; then + BT_ADAPTER=${WLAN_INTERFACES[0]} + show_message <<< "\nNo nearby Bluetooth devices were found!" + else + exec 3>&1 + BT_ADAPTER=$(whiptail --title "Select interface" \ + --clear --menu "" $((6+${LIST_LENGTH})) 50 $LIST_LENGTH "${LIST[@]}" 2>&1 1>&3) + exec 3>&- + if [[ $BT_ADAPTER != "" ]]; then + show_infobox <<< "\nConnecting to $BT_ADAPTER " + BT_EXEC=$( + expect -c 'set prompt "#";set address '$BT_ADAPTER';spawn bluetoothctl;expect -re $prompt;send "disconnect $address\r"; + sleep 1;send "remove $address\r";sleep 1;expect -re $prompt;send "scan on\r";sleep 8;send "scan off\r"; + expect "Controller";send "trust $address\r";sleep 2;send "pair $address\r";sleep 2;send "connect $address\r"; + send_user "\nShould be paired now.\r";sleep 2;send "quit\r";expect eof') + echo "$BT_EXEC" > /tmp/bt-connect-debug.log + if [[ $(echo "$BT_EXEC" | grep "Connection successful" ) != "" ]]; then + show_message <<< "\nYour device is ready to use!" + else + show_message <<< "\nError connecting. Try again!" + fi + fi + fi + +} + + +# Start of config ng + +module_options+=( +["set_colors,author"]="Joey Turner" +["set_colors,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L287" +["set_colors,feature"]="set_colors" +["set_colors,desc"]="Change the background color of the terminal or dialoge box" +["set_colors,example"]="set_colors 0-7" +["set_colors,doc_link"]="" +["set_colors,status"]="review" +) +# +# Function to set the tui colors +# +[[ -x "$(command -v whiptail)" ]] && DIALOG="whiptail" || exit 1 ; + +function set_colors() { + local color_code=$1 + + if [ "$DIALOG" = "whiptail" ]; then + set_newt_colors "$color_code" + #echo "color code: $color_code" | show_infobox ; + elif [ "$DIALOG" = "dialog" ]; then + set_term_colors "$color_code" + else + echo "Invalid dialog type" + return 1 + fi +} + + +# +# Function to set the colors for newt +# +function set_newt_colors() { + local color_code=$1 + case $color_code in + 0) color="black" ;; + 1) color="red" ;; + 2) color="green" ;; + 3) color="yellow" ;; + 4) color="blue" ;; + 5) color="magenta" ;; + 6) color="cyan" ;; + 7) color="white" ;; + 8) color="black" ;; + 9) color="red" ;; + *) return ;; + esac + export NEWT_COLORS="root=,$color" +} + + +# +# Function to set the colors for terminal +# +function set_term_colors() { + local color_code=$1 + case $color_code in + 0) color="\e[40m" ;; # black + 1) color="\e[41m" ;; # red + 2) color="\e[42m" ;; # green + 3) color="\e[43m" ;; # yellow + 4) color="\e[44m" ;; # blue + 5) color="\e[45m" ;; # magenta + 6) color="\e[46m" ;; # cyan + 7) color="\e[47m" ;; # white + *) echo "Invalid color code"; return 1 ;; + esac + echo -e "$color" +} + + +# +# Function to reset the colors +# +function reset_colors() { + echo -e "\e[0m" +} + + +module_options+=( +["generate_top_menu,author"]="Joey Turner" +["generate_top_menu,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L370" +["generate_top_menu,feature"]="generate_top_menu" +["generate_top_menu,desc"]="Build the main menu from a object" +["generate_top_menu,example"]="generate_top_menu 'json_data'" +["generate_top_menu,doc_link"]="" +["generate_top_menu,status"]="review" +) +# +# Function to generate the main menu from a JSON object +# +generate_top_menu() { + local json_data=$1 + local menu_options=() + while IFS= read -r id + do + IFS= read -r description + IFS= read -r requirements + # If the condition field is not empty and not null, run the function specified in the condition + if [[ -n $requirements && $requirements != "null" ]]; then + local condition_result=$(eval $requirements) + # If the function returns a truthy value, add the menu item to the menu + if [[ $condition_result ]]; then + menu_options+=("$id" " - $description ($something)") + fi + else + # If the condition field is empty or null, add the menu item to the menu + menu_options+=("$id" " - $description ") + fi + done < <(echo "$json_data" | jq -r '.menu[] | select(.show==true) | "\(.id)\n\(.description)\n\(.condition)"' || exit 1 ) + + set_colors 4 + + local OPTION=$($DIALOG --title "$TITLE" --menu "$BACKTITLE" 0 80 9 "${menu_options[@]}" 3>&1 1>&2 2>&3) + local exitstatus=$? + + if [ $exitstatus = 0 ]; then + if [ "$OPTION" == "" ]; then + exit 0 + fi + [[ -n "$debug" ]] && echo "$OPTION" + generate_menu "$OPTION" + fi + +# echo "Menu options: ${menu_options[@]}" + +} + + +module_options+=( +["generate_menu,author"]="Joey Turner" +["generate_menu,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L416" +["generate_menu,feature"]="generate_menu" +["generate_menu,desc"]="Generate a submenu from a parent_id" +["generate_menu,example"]="generate_menu 'parent_id'" +["generate_menu,doc_link"]="" +["generate_menu,status"]="review" +) +# +# Function to generate the submenu +# +function generate_menu() { + local parent_id=$1 + + # Get the submenu options for the current parent_id + local submenu_options=() + while IFS= read -r id + do + IFS= read -r description + submenu_options+=("$id" " - $description") + done < <(jq -r --arg parent_id "$parent_id" '.menu[] | select(.id==$parent_id) | .sub[]? | select(.show==true) | "\(.id)\n\(.description)"' <<< "$json_data") + + + local OPTION=$($DIALOG --title "$TITLE" --menu "$BACKTITLE" 0 80 9 "${submenu_options[@]}" \ + --ok-button Select --cancel-button Back 3>&1 1>&2 2>&3) + + local exitstatus=$? + + if [ $exitstatus = 0 ]; then + if [ "$OPTION" == "" ]; then + generate_top_menu + fi + # Check if the selected option has a submenu + local submenu_count=$(jq -r --arg id "$OPTION" '.menu[] | .. | objects | select(.id==$id) | .sub[]? | length' "$json_file") + submenu_count=${submenu_count:-0} # If submenu_count is null or empty, set it to 0 + if [ "$submenu_count" -gt 0 ]; then + # If it does, generate a new menu for the submenu + set_colors 2 # "$?" + [[ -n "$debug" ]] && echo "$OPTION" + generate_menu "$OPTION" + else + # If it doesn't, execute the command + [[ -n "$debug" ]] && echo "$OPTION" + execute_command "$OPTION" + fi + fi + + # echo "Submenu options: ${submenu_options[@]}" + +} + + +module_options+=( +["execute_command,author"]="Joey Turner" +["execute_command,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L464" +["execute_command,feature"]="execute_command" +["execute_command,desc"]="Needed by generate_menu" +["execute_command,example"]="" +["execute_command,doc_link"]="" +["execute_command,status"]="review" +) +# +# Function to execute the command +# +function execute_command() { + local id=$1 + local commands=$(jq -r --arg id "$id" '.menu[] | .. | objects | select(.id==$id) | .command[]' "$json_file") + for command in "${commands[@]}"; do + # Check if the command is not in the list of restricted commands + [[ -n "$debug" ]] && echo "$command" + eval "$command" + done + +} + + +module_options+=( +["show_message,author"]="Joey Turner" +["show_message,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#486" +["show_message,feature"]="show_message" +["show_message,desc"]="Display a message box" +["show_message,example"]="show_message <<< 'hello world' " +["show_message,doc_link"]="https://github.com/armbian/configng/wiki/interface" +["show_message,status"]="review" +) +# +# Function to display a message box +# +function show_message() { + # Read the input from the pipe + input=$(cat) + + # Display the "OK" message box with the input data + if [[ $DIALOG != "bash" ]]; then + $DIALOG --title "$BACKTITLE" --msgbox "$input" 0 0 + else + echo -e "$input" + read -p -r "Press [Enter] to continue..." + fi +} + + +module_options+=( +["show_infobox,author"]="Joey Turner" +["show_infobox,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#512" +["show_infobox,feature"]="show_infobox" +["show_infobox,desc"]="pipe line strings to a infobox " +["show_infobox,example"]="show_infobox <<< 'hello world' ; " +["show_infobox,doc_link"]="" +["show_infobox,status"]="review" +) +# +# Function to display an infobox with a message +# +function show_infobox() { + export TERM=ansi + local input + local BACKTITLE="$BACKTITLE" + local -a buffer # Declare buffer as an array + if [ -p /dev/stdin ]; then + while IFS= read -r line; do + buffer+=("$line") # Add the line to the buffer + # If the buffer has more than 10 lines, remove the oldest line + if (( ${#buffer[@]} > 18 )); then + buffer=("${buffer[@]:1}") + fi + # Display the lines in the buffer in the infobox + + TERM=ansi $DIALOG --title "$BACKTITLE" --infobox "$(printf "%s\n" "${buffer[@]}" )" 16 90 + sleep 0.5 + done + else + + input="$1" + TERM=ansi $DIALOG --title "$BACKTITLE" --infobox "$input" 6 80 + fi + echo -ne '\033[3J' # clear the screen +} + + +module_options+=( +["show_menu,author"]="Joey Turner" +["show_menu,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L550" +["show_menu,feature"]="show_menu" +["show_menu,desc"]="Display a menu from pipe" +["show_menu,example"]="show_menu <<< armbianmonitor -h ; " +["show_menu,doc_link"]="" +["show_menu,status"]="WIP" +) +# +# +# +show_menu(){ + + # Get the input and convert it into an array of options + inpu_raw=$(cat) + # Remove the lines befor -h + input=$(echo "$inpu_raw" | sed 's/-\([a-zA-Z]\)/\1/' | grep '^ [a-zA-Z] ' | grep -v '\[') + options=() + while read -r line; do + package=$(echo "$line" | awk '{print $1}') + description=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ *//') + options+=("$package" "$description") + done <<< "$input" + + # Display the menu and get the user's choice + [[ $DIALOG != "bash" ]] && choice=$($DIALOG --title "Menu" --menu "Choose an option:" 0 0 9 "${options[@]}" 3>&1 1>&2 2>&3) + + # Check if the user made a choice + if [ $? -eq 0 ]; then + echo "$choice" + else + exit 0 + fi + + } + + +module_options+=( +["get_user_continue,author"]="Joey Turner" +["get_user_continue,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L588" +["get_user_continue,feature"]="get_user_continue" +["get_user_continue,desc"]="Display a Yes/No dialog box and prosees continue/exit" +["get_user_continue,example"]="get_user_continue 'Do you wish to continue?' process_input" +["get_user_continue,doc_link"]="" +["get_user_continue,status"]="review" +) +# +# Function to display a Yes/No dialog box (WIP) +# +function get_user_continue() { + local message="$1" + local next_action="$2" + + if $($DIALOG --yesno "$message" 10 80 3>&1 1>&2 2>&3); then + $next_action + else + $next_action "No" + fi +} + + +menu_options+=( +["get_user_continue,author"]="Joey Turner" +["get_user_continue,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#612" +["get_user_continue,feature"]="process_input" +["get_user_continue,desc"]="used to process the user's choice paired with get_user_continue" +["get_user_continue,example"]="get_user_continue 'Do you wish to continue?' process_input" +["get_user_continue,status"]="review" +["get_user_continue,doc_link"]="" +) +# +# Function to process the user's choice paired with get_user_continue +# +function process_input() { + local input="$1" + if [ "$input" = "No" ]; then + exit 1 + fi +} + + +module_options+=( +["get_user_continue_secure,author"]="Joey Turner" +["get_user_continue_secure,ref_link"]="" +["get_user_continue_secure,feature"]="get_user_continue_secure" +["get_user_continue_secure,desc"]="Secure version of get_user_continue" +["get_user_continue_secure,example"]="get_user_continue_secure 'Do you wish to continue?' process_input" +["get_user_continue_secure,doc_link"]="" +["get_user_continue_secure,status"]="WIP" +) +# +# Secure version of get_user_continue +# +function get_user_continue_secure() { + local message="$1" + local next_action="$2" + + # Define a list of allowed functions + local allowed_functions=("process_input" "other_function") + + # Check if the next_action is in the list of allowed functions + if [[ " ${allowed_functions[@]} " =~ " ${next_action} " ]]; then + if $($DIALOG --yesno "$message" 10 80 3>&1 1>&2 2>&3); then + $next_action + else + $next_action "No" + fi + else + echo "Error: Invalid function" + + exit 1 + fi +} + + + +module_options+=( +["see_ping,author"]="Joey Turner" +["see_ping,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#632" +["see_ping,feature"]="see_ping" +["see_ping,desc"]="Check the internet connection with fallback DNS" +["see_ping,example"]="see_ping" +["see_ping,doc_link"]="" +["see_ping,status"]="review" +) +# +# Function to check the internet connection +# +function see_ping() { + # List of servers to ping + servers=("1.1.1.1" "8.8.8.8") + + # Check for internet connection + for server in "${servers[@]}"; do + if ping -q -c 1 -W 1 $server >/dev/null; then + echo "Internet connection: Present" + break + else + echo "Internet connection: Failed" + sleep 1 + fi + done + + if [[ $? -ne 0 ]]; then + read -n -r 1 -s -p "Warning: Configuration cannot work properly without a working internet connection. \ + Press CTRL C to stop or any key to ignore and continue." + fi + +} + + +module_options+=( +["see_current_apt,author"]="Joey Turner" +["see_current_apt,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#667" +["see_current_apt,feature"]="see_current_apt" +["see_current_apt,desc"]="Check when apt list was last updated" +["see_current_apt,example"]="see_current_apt" +["see_current_apt,doc_link"]="" +["see_current_apt,status"]="review" +) +# +# Function to check when the package list was last updated +# +see_current_apt() { + # Number of seconds in a day + local day=86400 + + # Get the current date as a Unix timestamp + local now=$(date +%s) + + # Get the timestamp of the most recently updated file in /var/lib/apt/lists/ + local update=$(stat -c %Y /var/lib/apt/lists/* | sort -n | tail -1) + + # Calculate the number of seconds since the last update + local elapsed=$(( now - update )) + + if ps -C apt-get,apt,dpkg >/dev/null; then + echo "A pkg is running." + export running_pkg="true" + return 1 # The processes are running + else + export running_pkg="false" + #echo "apt, apt-get, or dpkg is not currently running" + fi + # Check if the package list is up-to-date + if (( elapsed < day )); then + #echo "Checking for apt-daily.service" + echo "$(date -u -d @${elapsed} +"%T")" + return 0 # The package lists are up-to-date + else + #echo "Checking for apt-daily.service" + echo "Update the package lists" + return 1 # The package lists are not up-to-date + fi +} diff --git a/lib/armbian-configng/config.ng.jobs.json b/lib/armbian-configng/config.ng.jobs.json new file mode 100644 index 000000000..4dc2f552f --- /dev/null +++ b/lib/armbian-configng/config.ng.jobs.json @@ -0,0 +1,295 @@ +{ + "menu": [ + { + "id": "System", + "description": "System wide and admin settings", + "show": true, + "sub": [ + { + "id": "S01", + "description": "Enable Armbina kernal upgrades", + "command": [ + "get_user_continue \"This will allow apt to update boot critical items\n\n Continue?\" process_input", + "set_safe_boot unhold" + ], + "show": true, + "status": "WIP", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "S02", + "description": "Disable Armbina kernal upgrades", + "command": [ + "get_user_continue \"This will apt hold boot critical items\n\n Continue?\" process_input", + "set_safe_boot freeze" + ], + "show": true, + "status": "WIP", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "S03", + "description": "Edit the boot enviroment (WIP)", + "command": [ + "get_user_continue \"This will open /boot/armbianEnv.txt file to edit\nCTRL+S to save\nCTLR+X to exit\nwould you like to continue?\" process_input", + "nano /boot/armbianEnv.txt" + ], + "show": true, + "status": "WIP", + "doc_link": "", + "src_reference": "", + "author": "" + } + ] + }, + { + "id": "Network", + "description": "Wireless, Ethernet, and Network settings", + "show": true, + "sub": [ + { + "id": "BT0", + "description": "Install Bluetooth support", + "command": [ + "see_current_apt ", + "debconf-apt-progress -- apt-get -y install bluetooth bluez bluez-tools", + "check_if_installed xserver-xorg && debconf-apt-progress -- apt-get -y --no-install-recommends install pulseaudio-module-bluetooth blueman" + ], + "show": false, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "BT1", + "description": "Remove Bluetooth support", + "command": [ + "see_current_apt ", + "debconf-apt-progress -- apt-get -y remove bluetooth bluez bluez-tools", + "check_if_installed xserver-xorg && debconf-apt-progress -- apt-get -y remove pulseaudio-module-bluetooth blueman", + "debconf-apt-progress -- apt -y -qq autoremove" + ], + "show": false, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "BT3", + "description": "Bluetooth Discover", + "command": [ + "get_user_continue \"Verify that your Bluetooth device is discoverable!\" process_input ; connect_bt_interface" + + ], + "show": false, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "IR0", + "description": "Install Infrared support", + "command": [ + "see_current_apt; debconf-apt-progress -- apt-get -y --no-install-recommends install lirc" + ], + "show": false, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "IR1", + "description": "Uninstall Infrared support", + "command": [ + "see_current_apt; debconf-apt-progress -- apt-get -y --no-install-recommends install lirc" + ], + "show": false, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "N00", + "description": "Manage wifi network connections", + "command": [ + "nmtui connect" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "N01", + "description": "Advanced Edit /etc/network/interface", + "command": [ + "get_user_continue \"This will open interface file to edit\nCTRL+S to save\nCTLR+X to exit\nwould you like to continue?\" process_input", + "nano /etc/network/interfaces" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "N02", + "description": "Disconect and forget all wifi connections (Advanced)", + "command": [ + "get_user_continue \"Disconect and forget all wifi connections\nWould you like to contiue?\" process_input", + "LC_ALL=C nmcli --fields UUID,TIMESTAMP-REAL,TYPE con show | grep wifi | awk '{print $1}' | while read line; \\ ", + "do nmcli con delete uuid $line; done > /dev/null" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "N03", + "description": "Toggle system IPv6/IPv4 internet protical", + "command": [ + "get_user_continue \"This will toggle your internet protical\nWould you like to contiue?\" process_input", + "toggle_ipv6 | show_infobox" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + } + ] + }, + { + "id": "Localisation", + "description": "Localisation", + "show": true, + "sub": [ + { + "id": "L00", + "description": "Change Globla timezone (WIP)", + "command": [ + "dpkg-reconfigure tzdata" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "L01", + "description": "Change Locales reconfigure the language and charitorset", + "command": [ + "dpkg-reconfigure locales", + "source /etc/default/locale ; sed -i \"s/^LANGUAGE=.*/LANGUAGE=$LANG/\" /etc/default/locale", + "export LANGUAGE=$LANG" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "L02", + "description": "Change Keyboard layout", + "command": [ + "dpkg-reconfigure keyboard-configuration ; setupcon " + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "L03", + "description": "Change APT mirrors", + "command": [ + "get_user_continue \"This is only a frontend test\" process_input" + ], + "show": false, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + } + ] + }, + { + "id": "Software", + "description": "Run/Install 3rd party apllications", + "show": true, + "sub": [ + { + "id": "I00", + "description": "Update Application Repository", + "command": [ + "get_user_continue \"This will update apt\" process_input", + "debconf-apt-progress -- apt update" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "I01", + "description": "CLI System Monitor", + "command": [ + "armbianmonitor -m | show_infobox" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + } + ] + }, + { + "id": "Help", + "description": "About this app", + "show": true, + "sub": [ + { + "id": "H00", + "description": "About This systme. (WIP)", + "command": [ + "show_message <<< \"This app is to help exicute prosedures to configure your system\n\nSome option may not work on manualy modified sytemes\"" + ], + "show": true, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + }, + { + "id": "H02", + "description": "List of Config function(WIP)", + "command": [ + "show_message <<< see_use" + ], + "show": false, + "status": "review", + "doc_link": "", + "src_reference": "", + "author": "" + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/armbian-configng/config.ng.network.sh b/lib/armbian-configng/config.ng.network.sh new file mode 100644 index 000000000..27b0d2a43 --- /dev/null +++ b/lib/armbian-configng/config.ng.network.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +module_options+=( +["check_ip_version,author"]="Joey Turner" +["check_ip_version,ref_link"]="" +["check_ip_version,feature"]="check_ip_version" +["check_ip_version,desc"]="Check if a domain is reachable via IPv4 and IPv6" +["check_ip_version,example"]="check_ip_version google.com" +["check_ip_version,status"]="review" +["check_ip_version,doc_link"]="" +) +# +# +# +check_ip_version() { + domain=${1:-armbian.com} + + if ping -c 1 $domain > /dev/null 2>&1; then + echo "IPv4" + elif ping6 -c 1 $domain > /dev/null 2>&1; then + echo "IPv6" + else + echo "Unreachable" + fi +} + + + +module_options+=( +["toggle_ipv6,author"]="Joey Turner" +["toggle_ipv6,ref_link"]="" +["toggle_ipv6,feature"]="toggle_ipv6" +["toggle_ipv6,desc"]="Toggle IPv6 on or off" +["toggle_ipv6,example"]="toggle_ipv6" +["toggle_ipv6,status"]="review" +["toggle_ipv6,doc_link"]="" +) +# +# Function to toggle IPv6 on or off +# +toggle_ipv6() { + # Check if IPv6 is currently enabled + if sysctl net.ipv6.conf.all.disable_ipv6 | grep -q 0; then + # If IPv6 is enabled, disable it + echo "Disabling IPv6..." + sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 + sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1 + sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1 + echo "IPv6 is now disabled." + # Confirm that IPv6 is disabled + if sysctl net.ipv6.conf.all.disable_ipv6 | grep -q 1; then + check_ip_version google.com + else + check_ip_version google.com + fi + else + # If IPv6 is disabled, enable it + echo "Enabling IPv6..." + sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 + sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0 + sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=0 + echo "IPv6 is now enabled." + # Confirm that IPv6 is enabled + if sysctl net.ipv6.conf.all.disable_ipv6 | grep -q 0; then + check_ip_version google.com + else + check_ip_version google.com + fi + fi + + # Now call the function with a domain name + +} + +module_options+=( +["see_ping,author"]="Joey Turner" +["see_ping,ref_link"]="https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#632" +["see_ping,feature"]="see_ping" +["see_ping,desc"]="Check the internet connection with fallback DNS" +["see_ping,example"]="see_ping" +["see_ping,doc_link"]="" +["see_ping,status"]="review" +) +# +# Function to check the internet connection +# +function see_ping() { + # List of servers to ping + servers=("1.1.1.1" "8.8.8.8") + + # Check for internet connection + for server in "${servers[@]}"; do + if ping -q -c 1 -W 1 $server >/dev/null; then + echo "Internet connection: Present" + break + else + echo "Internet connection: Failed" + sleep 1 + fi + done + + if [[ $? -ne 0 ]]; then + read -n -r 1 -s -p "Warning: Configuration cannot work properly without a working internet connection. \ + Press CTRL C to stop or any key to ignore and continue." + fi + +} + diff --git a/lib/armbian-configng/config.ng.runtime.dev.sh b/lib/armbian-configng/config.ng.runtime.dev.sh new file mode 100644 index 000000000..4d840240e --- /dev/null +++ b/lib/armbian-configng/config.ng.runtime.dev.sh @@ -0,0 +1,45 @@ +#!/bin/bash + + +if [[ "$1" == "dev" || "$1" == "--dev" ]]; then + shift + get_user_continue "Development Mode:\n\nYou are entering development mode. System Administration features will be unavailable. Do you wish to continue?" process_input + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Development") .show) |= true') + + # sets backgrount to black + set_colors 0 + + # Append Items to main menu descriptions + json_data=$(echo "$json_data" | jq --arg str "$localisation" '(.menu[] | select(.id == "Personalisation" ) .description) += " (" + $str + ")"') + json_data=$(echo "$json_data" | jq --arg str "$install" '(.menu[] | select(.id == "Downloads" ) .description) += " (" + $str + ")"') + + + # Append Items to Sub menu descriptions + json_data=$(echo "$json_data" | jq --arg str "$network" '(.menu[] | select(.id=="Development").sub[] | select(.id == "T2").description) += " (" + $str + ")"') + json_data=$(echo "$json_data" | jq --arg str "$install" '(.menu[] | select(.id=="Development").sub[] | select(.id == "T1").description) += " (" + $str + ")"') + json_data=$(echo "$json_data" | jq --arg str "$install" '(.menu[] | select(.id=="Downloads").sub[] | select(.id == "I0").description) += " (" + $str + ")"') + + + # hide sys admin items from user menu + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="System") .show) |= false') ; + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Localisation") .show) |= false') ; + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Network") .show) |= false') ; + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Software") .show) |= false') ; + + + # show menu user level menu items + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Personalisation") .show) |= true') ; + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Help") .show) |= true') ; + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Downloads") .show) |= true') ; + +elif [[ "$1" == "--docs" ]]; then + generate_readme + exit 0 +elif [[ "$1" == "--help" ]]; then + see_use + exit 0 +else + # exit admin tool + echo -e "error: Exiting \nTry: 'sudo armbian-config'\n or: 'armbian-config --help' for More info\n\n" + exit 0 +fi diff --git a/lib/armbian-configng/config.ng.runtime.sh b/lib/armbian-configng/config.ng.runtime.sh new file mode 100644 index 000000000..6e334f081 --- /dev/null +++ b/lib/armbian-configng/config.ng.runtime.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# This script is used to dynamically modify a JSON structure that represents a menu in the Armbian configuration tool. +# It performs several checks, such as checking if certain packages are installed and determining the network protocol used. +# Based on these checks, it appends information to the descriptions of menu and submenu items, and shows or hides certain submenu items. +# The modified JSON structure is stored in the variable 'json_data'. + +set_colors 2 # Set the color to green + +# Main menu items +system="$(uname -m)" +network="$(echo "$DEFAULT_ADAPTER")" +localisation="$LANG" +software="$(see_current_apt)" + + +# Sub menu items +bluetooth_installed=$(dpkg -s bluetooth &> /dev/null && echo true || echo false) +bluez_installed=$(dpkg -s bluez &> /dev/null && echo true || echo false) +bluez_tools_installed=$(dpkg -s bluez-tools &> /dev/null && echo true || echo false) +#check_hold=$(apt-mark showhold) + +# Append Items to menu descriptions +json_data=$(echo "$json_data" | jq --arg str "$system" '(.menu[] | select(.id == "System" ) .description) += " (" + $str + ")"') +json_data=$(echo "$json_data" | jq --arg str "$network" '(.menu[] | select(.id == "Network" ) .description) += " (" + $str + ")"') +json_data=$(echo "$json_data" | jq --arg str "$localisation" '(.menu[] | select(.id == "Localisation" ) .description) += " (" + $str + ")"') +json_data=$(echo "$json_data" | jq --arg str "$software" '(.menu[] | select(.id == "Software" ) .description) += " (" + $str + ")"') + +# +# Append Items to Sub menu descriptions +json_data=$(echo "$json_data" | jq --arg str "$network" '(.menu[] | select(.id=="Testing").sub[] | select(.id == "T2").description) += " (" + $str + ")"') +json_data=$(echo "$json_data" | jq --arg str "$software" '(.menu[] | select(.id=="Testing").sub[] | select(.id == "T1").description) += " (" + $str + ")"') +json_data=$(echo "$json_data" | jq --arg str "$software" '(.menu[] | select(.id=="Install").sub[] | select(.id == "I0").description) += " (" + $str + ")"') + +# Show or hide Sub menu items dynamicly + +if [ "$network" = "IPv6" ]; then + # If IPv6 is being used, do something + json_data=$(echo "$json_data" | jq --arg str "IPV6" '(.menu[] | select(.id=="Network").sub[] | select(.id == "N03").description) += " (" + $str + ")"') +else + # If IPv4 is being used or the domain is unreachable, do something else + json_data=$(echo "$json_data" | jq --arg str "IPV4" '(.menu[] | select(.id=="Network").sub[] | select(.id == "N03").description) += " (" + $str + ")"') +fi + +if [ "$bluetooth_installed" = false ] || [ "$bluez_installed" = false ] || [ "$bluez_tools_installed" = false ]; then + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Network").sub[] | select(.id == "BT0").show) |= true') + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Network").sub[] | select(.id == "BT3").show) |= false') + +else + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Network").sub[] | select(.id == "BT1").show) |= true') + json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="Network").sub[] | select(.id == "BT3").show) |= true') +fi + +# Show or hide Sub menu items dynamicly +# + +[[ -n "$check_hold" ]] && json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="System").sub[] | select(.id == "S03").show) |= true') +[[ -z "$check_hold" ]] && json_data=$(echo "$json_data" | jq '(.menu[] | select(.id=="System").sub[] | select(.id == "S04").show) |= true') + + diff --git a/share/doc/armbian-configng/Functions.md b/share/doc/armbian-configng/Functions.md new file mode 100644 index 000000000..de99d4987 --- /dev/null +++ b/share/doc/armbian-configng/Functions.md @@ -0,0 +1,35 @@ + +# Helper functions +A list of the heper function ie bash prosedures used in Jobs file. + +| Feature | Description | Credit | Reference | Documents | Status | +|:------- | ----------- | ----------- |:---------:|:---------:|:------:| +| set_runtime_variables | Run time varibales Migrated procedures from Armbian config. | Igor Pecovnik | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L136) | X | review | +| see_function_table_md | Generate this markdown table of all module_options | Tearran | X | X | source link Needed | +| show_menu | Display a menu from pipe | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L550) | X | WIP | +| generate_top_menu | Build the main menu from a object | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L370) | X | review | +| is_package_manager_running | Migrated procedures from Armbian config. | Igor Pecovnik | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L111) | X | review | +| check_desktop | Migrated procedures from Armbian config. | Igor Pecovnik | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L16) | X | review | +| generate_readme | Generate Document files. | Joey Turner | [Source](https://github.com/armbian/configng/blob/main/lib/armbian-configng/documents.sh#L18) | X | review | +| execute_command | Needed by generate_menu | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L464) | X | review | +| get_user_continue | Display a Yes/No dialog box and prosees continue/exit | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L588) | X | review | +| show_message | Display a message box | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#486) | [Document](https://github.com/armbian/configng/wiki/interface) | Review | +| connect_bt_interface | Migrated procedures from Armbian config. | Igor Pecovnik | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L221) | X | review | +| set_safe_boot | Freeze/unhold Migrated procedures from Armbian config. | Igor Pecovnik | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L188) | X | review | +| see_current_apt | Check when apt list was last updated | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#667) | X | review | +| check_if_installed | Migrated procedures from Armbian config. | Igor Pecovnik | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L88) | X | review | +| generate_svg | Generate 'Armbian CPU logo' SVG for docunment file. | Tearran | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#201) | X | review | +| check_ip_version | Check if a domain is reachable via IPv4 and IPv6 | Joey Turner | X | X | source link Needed | +| set_header_remove | Migrated procedures from Armbian config. | Igor Pecovnik | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L64) | X | review | +| generate_menu | Generate a submenu from a parent_id | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L416) | X | review | +| see_jq_menu_list | Generate a markdown list json objects using jq. | Tearran | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L304) | X | review | +| generate_jobs_from_json | Generate jobs from JSON file. | Tearran | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L223) | X | review | +| toggle_ipv6 | Toggle IPv6 on or off | Joey Turner | X | X | source link Needed | +| generate_json | Generate JSON-like object file. | Tearran | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L149) | X | review | +| set_colors | Change the background color of the terminal or dialoge box | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L287) | X | review | +| serve_doc | Serve the edit and debug server. | Tearran | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L89) | X | review | +| show_infobox | pipe line strings to a infobox | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#512) | X | review | +| see_use | Show the usage of the functions. | Tearran | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#L126) | X | review | +| see_ping | Check the internet connection with fallback DNS | Joey Turner | [Source](https://github.com/Tearran/configng/blob/main/config.ng.functions.sh#632) | X | review | +| get_user_continue_secure | Secure version of get_user_continue | Joey Turner | X | X | source link Needed | + diff --git a/share/doc/armbian-configng/Home.md b/share/doc/armbian-configng/Home.md new file mode 100644 index 000000000..1e772cd04 --- /dev/null +++ b/share/doc/armbian-configng/Home.md @@ -0,0 +1,90 @@ + +# Armbian configuration utility +Utility for configuring your board, divided into four main sections: + +- System - system and security settings, +- Network - wired, wireless, Bluetooth, access point, +- Personal - timezone, language, hostname, +- Software - system and 3rd party software install. + + + +To Configure and change global sytem settings, run the following command: `./armbian-configng` + +*** + +Following was updated on: +Thu Apr 11 02:23:43 AM MST 2024. + +*** +- ## **System** + - **S01** - Description: Enable Armbina kernal upgrades + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s01) + - **S02** - Description: Disable Armbina kernal upgrades + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s02) + - **S03** - Description: Edit the boot enviroment (WIP) + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s03) + + +- ## **Network** + - **BT0** - Description: Install Bluetooth support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt0) + - **BT1** - Description: Remove Bluetooth support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt1) + - **BT3** - Description: Bluetooth Discover + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt3) + - **IR0** - Description: Install Infrared support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#ir0) + - **IR1** - Description: Uninstall Infrared support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#ir1) + - **N00** - Description: Manage wifi network connections + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n00) + - **N01** - Description: Advanced Edit /etc/network/interface + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n01) + - **N02** - Description: Disconect and forget all wifi connections (Advanced) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n02) + - **N03** - Description: Toggle system IPv6/IPv4 internet protical + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n03) + + +- ## **Localisation** + - **L00** - Description: Change Globla timezone (WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l00) + - **L01** - Description: Change Locales reconfigure the language and charitorset + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l01) + - **L02** - Description: Change Keyboard layout + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l02) + - **L03** - Description: Change APT mirrors + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l03) + + +- ## **Software** + - **I00** - Description: Update Application Repository + - Status: [review](https://github.com/armbian/configng/wiki/Menu#i00) + - **I01** - Description: CLI System Monitor + - Status: [review](https://github.com/armbian/configng/wiki/Menu#i01) + + +- ## **Help** + - **H00** - Description: About This systme. (WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#h00) + - **H02** - Description: List of Config function(WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#h02) + + +*** + +# Development +get the lastet version of the utility by running the following command: + +~~~ +git clone https://github.com/armbian/configng.git +cd configng +~~~ + + + +## Note: +> +> The Bash procedures embedded within the JSON structure are meticulously designed with a focus on clear naming conventions and the simplicity of key pairs. These procedures serve multiple purposes, including facilitating the generation of content in various formats, such as Whiptail, Markdown, json out and others. Moreover, they are utilized for evaluation and execution of commands outlined in the JSON structure. +> diff --git a/share/doc/armbian-configng/Menu.md b/share/doc/armbian-configng/Menu.md new file mode 100644 index 000000000..8e67088d4 --- /dev/null +++ b/share/doc/armbian-configng/Menu.md @@ -0,0 +1,233 @@ + +# Armbian-config Menu list. +armbian-config jobs list. + +### S01 + +Enable Armbina kernal upgrades + +Jobs: + +~~~ +get_user_continue "This will allow apt to update boot critical items + + Continue?" process_input +set_safe_boot unhold +~~~ + +### S02 + +Disable Armbina kernal upgrades + +Jobs: + +~~~ +get_user_continue "This will apt hold boot critical items + + Continue?" process_input +set_safe_boot freeze +~~~ + +### S03 + +Edit the boot enviroment (WIP) + +Jobs: + +~~~ +get_user_continue "This will open /boot/armbianEnv.txt file to edit +CTRL+S to save +CTLR+X to exit +would you like to continue?" process_input +nano /boot/armbianEnv.txt +~~~ + +### BT0 + +Install Bluetooth support + +Jobs: + +~~~ +see_current_apt +debconf-apt-progress -- apt-get -y install bluetooth bluez bluez-tools +check_if_installed xserver-xorg && debconf-apt-progress -- apt-get -y --no-install-recommends install pulseaudio-module-bluetooth blueman +~~~ + +### BT1 + +Remove Bluetooth support + +Jobs: + +~~~ +see_current_apt +debconf-apt-progress -- apt-get -y remove bluetooth bluez bluez-tools +check_if_installed xserver-xorg && debconf-apt-progress -- apt-get -y remove pulseaudio-module-bluetooth blueman +debconf-apt-progress -- apt -y -qq autoremove +~~~ + +### BT3 + +Bluetooth Discover + +Jobs: + +~~~ +get_user_continue "Verify that your Bluetooth device is discoverable!" process_input ; connect_bt_interface +~~~ + +### IR0 + +Install Infrared support + +Jobs: + +~~~ +see_current_apt; debconf-apt-progress -- apt-get -y --no-install-recommends install lirc +~~~ + +### IR1 + +Uninstall Infrared support + +Jobs: + +~~~ +see_current_apt; debconf-apt-progress -- apt-get -y --no-install-recommends install lirc +~~~ + +### N00 + +Manage wifi network connections + +Jobs: + +~~~ +nmtui connect +~~~ + +### N01 + +Advanced Edit /etc/network/interface + +Jobs: + +~~~ +get_user_continue "This will open interface file to edit +CTRL+S to save +CTLR+X to exit +would you like to continue?" process_input +nano /etc/network/interfaces +~~~ + +### N02 + +Disconect and forget all wifi connections (Advanced) + +Jobs: + +~~~ +get_user_continue "Disconect and forget all wifi connections +Would you like to contiue?" process_input +LC_ALL=C nmcli --fields UUID,TIMESTAMP-REAL,TYPE con show | grep wifi | awk '{print $1}' | while read line; \ +do nmcli con delete uuid $line; done > /dev/null +~~~ + +### N03 + +Toggle system IPv6/IPv4 internet protical + +Jobs: + +~~~ +get_user_continue "This will toggle your internet protical +Would you like to contiue?" process_input +toggle_ipv6 | show_infobox +~~~ + +### L00 + +Change Globla timezone (WIP) + +Jobs: + +~~~ +dpkg-reconfigure tzdata +~~~ + +### L01 + +Change Locales reconfigure the language and charitorset + +Jobs: + +~~~ +dpkg-reconfigure locales +source /etc/default/locale ; sed -i "s/^LANGUAGE=.*/LANGUAGE=$LANG/" /etc/default/locale +export LANGUAGE=$LANG +~~~ + +### L02 + +Change Keyboard layout + +Jobs: + +~~~ +dpkg-reconfigure keyboard-configuration ; setupcon +~~~ + +### L03 + +Change APT mirrors + +Jobs: + +~~~ +get_user_continue "This is only a frontend test" process_input +~~~ + +### I00 + +Update Application Repository + +Jobs: + +~~~ +get_user_continue "This will update apt" process_input +debconf-apt-progress -- apt update +~~~ + +### I01 + +CLI System Monitor + +Jobs: + +~~~ +armbianmonitor -m | show_infobox +~~~ + +### H00 + +About This systme. (WIP) + +Jobs: + +~~~ +show_message <<< "This app is to help exicute prosedures to configure your system + +Some option may not work on manualy modified sytemes" +~~~ + +### H02 + +List of Config function(WIP) + +Jobs: + +~~~ +show_message <<< see_use +~~~ + diff --git a/share/doc/armbian-configng/ScreenShot.md b/share/doc/armbian-configng/ScreenShot.md new file mode 100644 index 000000000..f82a980d0 --- /dev/null +++ b/share/doc/armbian-configng/ScreenShot.md @@ -0,0 +1,17 @@ +# Wiptail +## usage +### Ok message box +image + + +the following will output the boot up time +```bash + systemd-analyze | show_message +``` +image + +### Menu selection box +```bash +apt-cache search desktop | grep -i -e "\-desktop-full " -e "\-desktop-environment " | awk -F "- " '{print $1, $2}' | show_menu +``` +(WIP) \ No newline at end of file diff --git a/share/doc/armbian-configng/Standards.md b/share/doc/armbian-configng/Standards.md new file mode 100644 index 000000000..ffe5d857b --- /dev/null +++ b/share/doc/armbian-configng/Standards.md @@ -0,0 +1,37 @@ +# Naming Convention + +## Categories +1. Network - Ethernet Wireless Bluetooth Access Point +2. Locales - Locale Language Region Time Keyboard +3. System - System and Security +4. SoftWare - Third-party applications + +# Function Naming Convention + +This project uses the following naming convention for functions: +## Admin sudo user +### main function groups system and security +- `see_`: used for retrieving or viewing values `apt-cashe grep something` `ls -h` `cat file.txt` `lsblk` +- `set_`: used for setting or updating values `echo "somevalue" > somefile.txt` +- `get_`: used for getting downloads or updates `apt-get install something` +- `rem_`: used for removing or uninstalling something `apt-get purge something` +## Non Admin non sudo +### user space, end-user Customization +- `run_`: used for running apps in the user space `/usr/bin/chromium --kiosk https://forum.armbian.com/ https://github.com/armbian/configng &` +- `mod_`: used for modifying or getting something in user space `git clone` `wget` + +Please use these prefixes consistently when naming functions in this project. + +# Help message format +## Existing Example +- `ls --help` Shows advanced flag options +- `p7zip -h` Shows simple flag options +- `git --help` Shows advanced non flag options + +## Base Example. +potentially build to any of the with previous +```bash + +``` + + From 93e87c6d288f1c67f63e40db5ee2cefabe2800fd Mon Sep 17 00:00:00 2001 From: Tearran Date: Fri, 12 Apr 2024 00:32:27 -0700 Subject: [PATCH 46/48] Renamed scripts configng and armbian-interface to confignng+ cli / tui --- bin/armbian-configng | 244 +++++------------------- bin/configng | 78 -------- bin/configng-cli | 222 +++++++++++++++++++++ bin/{armbian-interface => configng-tui} | 0 4 files changed, 272 insertions(+), 272 deletions(-) mode change 100755 => 100644 bin/armbian-configng delete mode 100644 bin/configng create mode 100755 bin/configng-cli rename bin/{armbian-interface => configng-tui} (100%) diff --git a/bin/armbian-configng b/bin/armbian-configng old mode 100755 new mode 100644 index 2a6c89774..1ae72391c --- a/bin/armbian-configng +++ b/bin/armbian-configng @@ -1,222 +1,78 @@ #!/bin/bash -# This script provides a command-line interface for managing Armbian configuration. -# It loads libraries of functions from the lib directory and displays them in a menu. -# The user can select a function to run, or use a text-based user interface (TUI) to navigate the menus. -# The script also provides a help option and a debug option for developers. -# The script requires sudo privileges to run some functions. -# The script uses whiptail or dialog for the TUI, depending on which is available. - -#set -x -#set -e +tput init # -# Enable Dynamic directory root use home ~/ , /bin , /usr/sbin etc.. -bin="$(dirname "${BASH_SOURCE[0]}")" -directory="$(cd "$bin/../" && pwd )" -file_name="$(basename "${BASH_SOURCE[0]}")" -filename="${file_name%.*}" -libpath=$(cd "$directory/lib/$filename/" && pwd) -#sharepath=$(cd "$directory/share/${filename%-dev}/" && pwd) - +# Language-based variable assignment for script directory path +# This serves as a Rosetta Stone for developers, +# allowing them to use the variable name they are most comfortable with. -# -# Consept Distribution Compatibility checks -check_distro() { +# allows CTRL c to exit +trap "exit" INT TERM - [[ -f "/usr/bin/${filename%%-*}-config" ]] && distro_config="${filename%%-*}" - [[ -f "/etc/${filename%%-*}-release" ]] && distro_release="${filename%%-*}" - # if both true then we are good to go - [[ -z "$distro_config" ]] || [[ -z "$distro_release" ]] && echo "W: Costum build, Tech support links are missing." - [[ -n "$distro_config" ]] && [[ -n "$distro_release" ]] && echo "I: This build seems to be community supported" | ./armbian-interface -o - [[ -f "/usr/sbin/${filename%%-*}-config" ]] && distro_config="${filename%%-*}" - [[ -f "/etc/${filename%%-*}-release" ]] && distro_release="${filename%%-*}" +# Get the script directory +script_dir="$(dirname "$0")" -} - -[[ "$1" == "--dev" ]] && dev=1 && shift 1 +# Define the lib directory one level up from the script directory +lib_dir="$script_dir/../lib/armbian-configng" +doc_dir="$script_dir/../share/doc/armbian-configng" +# Check for the existence of the config.ng.jobs.json file in the lib directory +json_file="$lib_dir/config.ng.jobs.json" # -# Check if the script is dev version. -suffix="${file_name##*-}" +# Load The Bash procedure Objects +json_data=$(cat "$json_file") -if [[ "$suffix" == dev ]]; then - dev=1 - check_distro #| armbian-interface -o -fi -if [[ "$(id -u)" != "0" ]] && [[ "$dev" == "1" ]] ; then - -cat << EOF #| ./armbian-interface -o -I: Running in UX development mode -W: Admin functions will not work as expected +# +# We're checking if the 'whiptail' command is available on the system. +# 'whiptail' is a simple dialog box utility that works well with Bash. It doesn't have all the features of some other dialog box utilities, but it does everything we need for this script. +# If 'whiptail' is available, we assign its name to the variable DIALOG. This way, we can easily switch to another dialog box utility in the future if we need to, just by changing this one line of code. +[[ -x "$(command -v whiptail)" ]] && DIALOG="whiptail" -EOF -elif [[ "$(id -u)" == "0" ]] && [[ "$dev" == "1" ]] ; then -cat << EOF | ./armbian-interface -o -I: Running in UX development mode -W: Document files may need Admin privleges to edit/remove -EOF +# +# Prepare the module options array +declare -A module_options -fi # -# Check if the script is being run as root -# UX Development mode bypasses root check, many functions will not work as expected +# Load configng core functions and module options array -if [[ "$(id -u)" != "0" ]] && [[ "$dev" != "1" ]]; then - echo -e "E: This tool requires root privileges. Try: \"sudo $filename\"" >&2 - exit 1 -fi -declare -A dialogue +source "$lib_dir/config.ng.functions.sh" +set_runtime_variables +echo "Loaded Runtime variables..." | show_infobox ; +set_newt_colors 2 +echo "Loaded Dialog..." | show_infobox ; +source "$lib_dir/config.ng.docs.sh" +echo "Loaded Docs..." | show_infobox ; +source "$lib_dir/config.ng.network.sh" +echo "Loaded Network helpers..." | show_infobox ; -# -# Check if whiptail or dialog is installed and set the variable 'dialogue' accordingly. -# todo add a fallback TUI and GUI -if command -v whiptail &> /dev/null; then - dialogue="whiptail" -elif command -v dialog &> /dev/null; then - dialogue="dialog" -else - echo "TUI not found" - echo "Warning: Using fallback TUI" - sleep 1 - clear && generate_read -fi - -source "$libpath/functions.sh" -source "$libpath/documents.sh" -for file in "$libpath"/*/*.sh; do - source "$file" -done # -# mapfile -t categories < <(ls -d "$libpath"/* ) -mapfile -t categories < <(find "$libpath"/* -type d) -declare -A functions - -for category in "${categories[@]}"; do - category_name="${category##*/}" - - category_file="$category/readme.md" - if [[ -f "$category_file" ]]; then - category_description=$(grep -oP "(?<=# @description ).*" "$category_file") - fi - - for file in "$category"/*.sh; do - description="" - while IFS= read -r line; do - if [[ $line =~ ^#\ @description\ (.*)$ ]]; then - description="${BASH_REMATCH[1]}" - elif [[ $line =~ ^function\ (.*::.*)\(\)\{$ ]]; then - # END: be15d9bcejpp - function_name="${BASH_REMATCH[1]}" - key="$category_name:${file##*/}:${function_name}" - functions["$key,function_name"]=$(echo "$function_name" | sed 's/.*:://') - functions["$key,group_name"]=$(echo "$function_name" | sed 's/::.*//') - functions["$key,description"]=$description - elif [[ $line =~ ^#\ @options\ (.*)$ ]]; then - functions["$key,options"]="${BASH_REMATCH[1]}" - fi - done < "$file" - functions["$key,category"]=$category_name - functions["$key,category_description"]=$category_description - done -done +# Loads the varibles from beta armbian-config for runtime handeling +source "$lib_dir/config.ng.runtime.sh" ; +echo "Loaded Runtime conditions..." | show_infobox ; # -# WIP: Check arguments for no flag options -# armbian-config --help -# Change to BASH: /usr/sbin/armbian-config main=System selection=BASH -handle_no_flag(){ -if [[ "$1" == *"="* ]]; then - IFS='=' read -r key value <<< "$1" - function_name=$(parse_action "$key" "$value") - # Call the function using variable indirection - ${function_name} -elif [[ "$1" == "help"* ]]; then - generate_list_cli +# if not sudo +# Runtime "include this script" for USER and development setup condistion +if [[ $EUID != 0 ]]; then + source "$lib_dir/config.ng.runtime.dev.sh" ; + echo "Loaded Develoment +Runtime conditions..." | show_infobox ; + fi -} +tput clear # -# Check arguments for long flag options -# Help message related to the functions the back end -handle_long_flag(){ - if [[ "$1" == "--help" ]]; then - generate_list_run - exit 0 ; - elif [[ "$1" == "--doc" ]]; then - generate_doc - exit 0 ; - fi -# WIP: - if [ "$1" == "--run" ]; then - shift # Shifts the arguments to the left, excluding the first argument ("-r") - group_name="$1" # Takes the first argument as the group name - shift 1 # Shifts the arguments again to exclude the group name - - function_name=$(parse_action "$group_name" "$1") - if [ $? -eq 0 ]; then - # Call the function using variable indirection - ${function_name} - fi - elif [ "$1" == "--help" ]; then - generate_list_run - exit - elif [ "$1" == "--test" ]; then - check_distro | armbian-interface -o && $1 > /dev/null - fi - -} -# -# Check arguments for short flag options -# THe interface help message -handle_short_flag(){ -if [ "$1" == "-h" ]; then - generate_help - exit 0 ; -# Generate a text-based user interface -elif [ "$1" == "-t" ] ; then - generate_read ; exit 0 ; -# Generate all doc files -elif [ "$1" == "-d" ] ; then - generate_doc ; exit 0 ; -elif [ "$1" == "-j" ] ; then - generate_json ; exit 0 ; -fi +# Generate the top menu with the modified Object data +while generate_top_menu "$json_data"; do tput clear ; done -} - -case "$1" in - *"="*) - # Handle the case where $1 contains "=" - handle_no_flag "$@" - ;; - *"--"*) - # Handle the case where $1 starts with "--" - handle_long_flag "$@" - ;; - *"-"*) - # Handle the case where $1 starts with "-" - handle_short_flag "$1" - ;; - *) - handle_no_flag "$@" - # Handle the case where $1 does not match any of the above patterns - # You can add your code here - ;; -esac - -if [[ -z "$1" ]] ; then - while true; do - generate_tui ; - if [[ "$?" == "0" ]]; then - exit 0 - fi - done - -fi \ No newline at end of file + +# +# Exit the script with a success status code +exit 0 diff --git a/bin/configng b/bin/configng deleted file mode 100644 index 1ae72391c..000000000 --- a/bin/configng +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - - -tput init -# -# Language-based variable assignment for script directory path -# This serves as a Rosetta Stone for developers, -# allowing them to use the variable name they are most comfortable with. - -# allows CTRL c to exit -trap "exit" INT TERM - -# Get the script directory -script_dir="$(dirname "$0")" - -# Define the lib directory one level up from the script directory -lib_dir="$script_dir/../lib/armbian-configng" -doc_dir="$script_dir/../share/doc/armbian-configng" -# Check for the existence of the config.ng.jobs.json file in the lib directory -json_file="$lib_dir/config.ng.jobs.json" - -# -# Load The Bash procedure Objects -json_data=$(cat "$json_file") - - -# -# We're checking if the 'whiptail' command is available on the system. -# 'whiptail' is a simple dialog box utility that works well with Bash. It doesn't have all the features of some other dialog box utilities, but it does everything we need for this script. -# If 'whiptail' is available, we assign its name to the variable DIALOG. This way, we can easily switch to another dialog box utility in the future if we need to, just by changing this one line of code. -[[ -x "$(command -v whiptail)" ]] && DIALOG="whiptail" - - -# -# Prepare the module options array -declare -A module_options - - -# -# Load configng core functions and module options array - - -source "$lib_dir/config.ng.functions.sh" -set_runtime_variables -echo "Loaded Runtime variables..." | show_infobox ; -set_newt_colors 2 -echo "Loaded Dialog..." | show_infobox ; -source "$lib_dir/config.ng.docs.sh" -echo "Loaded Docs..." | show_infobox ; -source "$lib_dir/config.ng.network.sh" -echo "Loaded Network helpers..." | show_infobox ; - - -# -# Loads the varibles from beta armbian-config for runtime handeling - -source "$lib_dir/config.ng.runtime.sh" ; -echo "Loaded Runtime conditions..." | show_infobox ; - -# -# if not sudo -# Runtime "include this script" for USER and development setup condistion -if [[ $EUID != 0 ]]; then - source "$lib_dir/config.ng.runtime.dev.sh" ; - echo "Loaded Develoment -Runtime conditions..." | show_infobox ; - -fi - -tput clear -# -# Generate the top menu with the modified Object data -while generate_top_menu "$json_data"; do tput clear ; done - - -# -# Exit the script with a success status code -exit 0 diff --git a/bin/configng-cli b/bin/configng-cli new file mode 100755 index 000000000..2a6c89774 --- /dev/null +++ b/bin/configng-cli @@ -0,0 +1,222 @@ +#!/bin/bash + +# This script provides a command-line interface for managing Armbian configuration. +# It loads libraries of functions from the lib directory and displays them in a menu. +# The user can select a function to run, or use a text-based user interface (TUI) to navigate the menus. +# The script also provides a help option and a debug option for developers. +# The script requires sudo privileges to run some functions. +# The script uses whiptail or dialog for the TUI, depending on which is available. + +#set -x +#set -e + +# +# Enable Dynamic directory root use home ~/ , /bin , /usr/sbin etc.. +bin="$(dirname "${BASH_SOURCE[0]}")" +directory="$(cd "$bin/../" && pwd )" +file_name="$(basename "${BASH_SOURCE[0]}")" +filename="${file_name%.*}" +libpath=$(cd "$directory/lib/$filename/" && pwd) +#sharepath=$(cd "$directory/share/${filename%-dev}/" && pwd) + + +# +# Consept Distribution Compatibility checks +check_distro() { + + [[ -f "/usr/bin/${filename%%-*}-config" ]] && distro_config="${filename%%-*}" + [[ -f "/etc/${filename%%-*}-release" ]] && distro_release="${filename%%-*}" + # if both true then we are good to go + [[ -z "$distro_config" ]] || [[ -z "$distro_release" ]] && echo "W: Costum build, Tech support links are missing." + [[ -n "$distro_config" ]] && [[ -n "$distro_release" ]] && echo "I: This build seems to be community supported" | ./armbian-interface -o + [[ -f "/usr/sbin/${filename%%-*}-config" ]] && distro_config="${filename%%-*}" + [[ -f "/etc/${filename%%-*}-release" ]] && distro_release="${filename%%-*}" + +} + +[[ "$1" == "--dev" ]] && dev=1 && shift 1 + +# +# Check if the script is dev version. +suffix="${file_name##*-}" + +if [[ "$suffix" == dev ]]; then + dev=1 + check_distro #| armbian-interface -o +fi + +if [[ "$(id -u)" != "0" ]] && [[ "$dev" == "1" ]] ; then + +cat << EOF #| ./armbian-interface -o +I: Running in UX development mode +W: Admin functions will not work as expected + +EOF +elif [[ "$(id -u)" == "0" ]] && [[ "$dev" == "1" ]] ; then +cat << EOF | ./armbian-interface -o +I: Running in UX development mode +W: Document files may need Admin privleges to edit/remove + +EOF + +fi + +# +# Check if the script is being run as root +# UX Development mode bypasses root check, many functions will not work as expected + +if [[ "$(id -u)" != "0" ]] && [[ "$dev" != "1" ]]; then + echo -e "E: This tool requires root privileges. Try: \"sudo $filename\"" >&2 + exit 1 +fi + +declare -A dialogue + +# +# Check if whiptail or dialog is installed and set the variable 'dialogue' accordingly. +# todo add a fallback TUI and GUI +if command -v whiptail &> /dev/null; then + dialogue="whiptail" +elif command -v dialog &> /dev/null; then + dialogue="dialog" +else + echo "TUI not found" + echo "Warning: Using fallback TUI" + sleep 1 + clear && generate_read +fi + +source "$libpath/functions.sh" +source "$libpath/documents.sh" +for file in "$libpath"/*/*.sh; do + source "$file" +done + +# +# mapfile -t categories < <(ls -d "$libpath"/* ) +mapfile -t categories < <(find "$libpath"/* -type d) +declare -A functions + +for category in "${categories[@]}"; do + category_name="${category##*/}" + + category_file="$category/readme.md" + if [[ -f "$category_file" ]]; then + category_description=$(grep -oP "(?<=# @description ).*" "$category_file") + fi + + for file in "$category"/*.sh; do + description="" + while IFS= read -r line; do + if [[ $line =~ ^#\ @description\ (.*)$ ]]; then + description="${BASH_REMATCH[1]}" + elif [[ $line =~ ^function\ (.*::.*)\(\)\{$ ]]; then + # END: be15d9bcejpp + function_name="${BASH_REMATCH[1]}" + key="$category_name:${file##*/}:${function_name}" + functions["$key,function_name"]=$(echo "$function_name" | sed 's/.*:://') + functions["$key,group_name"]=$(echo "$function_name" | sed 's/::.*//') + functions["$key,description"]=$description + elif [[ $line =~ ^#\ @options\ (.*)$ ]]; then + functions["$key,options"]="${BASH_REMATCH[1]}" + fi + done < "$file" + functions["$key,category"]=$category_name + functions["$key,category_description"]=$category_description + done +done + + +# +# WIP: Check arguments for no flag options +# armbian-config --help +# Change to BASH: /usr/sbin/armbian-config main=System selection=BASH +handle_no_flag(){ +if [[ "$1" == *"="* ]]; then + IFS='=' read -r key value <<< "$1" + function_name=$(parse_action "$key" "$value") + # Call the function using variable indirection + ${function_name} +elif [[ "$1" == "help"* ]]; then + generate_list_cli +fi +} + +# +# Check arguments for long flag options +# Help message related to the functions the back end +handle_long_flag(){ + if [[ "$1" == "--help" ]]; then + generate_list_run + exit 0 ; + elif [[ "$1" == "--doc" ]]; then + generate_doc + exit 0 ; + fi +# WIP: + if [ "$1" == "--run" ]; then + shift # Shifts the arguments to the left, excluding the first argument ("-r") + group_name="$1" # Takes the first argument as the group name + shift 1 # Shifts the arguments again to exclude the group name + + function_name=$(parse_action "$group_name" "$1") + if [ $? -eq 0 ]; then + # Call the function using variable indirection + ${function_name} + fi + elif [ "$1" == "--help" ]; then + generate_list_run + exit + elif [ "$1" == "--test" ]; then + check_distro | armbian-interface -o && $1 > /dev/null + fi + +} +# +# Check arguments for short flag options +# THe interface help message +handle_short_flag(){ +if [ "$1" == "-h" ]; then + generate_help + exit 0 ; +# Generate a text-based user interface +elif [ "$1" == "-t" ] ; then + generate_read ; exit 0 ; +# Generate all doc files +elif [ "$1" == "-d" ] ; then + generate_doc ; exit 0 ; +elif [ "$1" == "-j" ] ; then + generate_json ; exit 0 ; +fi + +} + +case "$1" in + *"="*) + # Handle the case where $1 contains "=" + handle_no_flag "$@" + ;; + *"--"*) + # Handle the case where $1 starts with "--" + handle_long_flag "$@" + ;; + *"-"*) + # Handle the case where $1 starts with "-" + handle_short_flag "$1" + ;; + *) + handle_no_flag "$@" + # Handle the case where $1 does not match any of the above patterns + # You can add your code here + ;; +esac + +if [[ -z "$1" ]] ; then + while true; do + generate_tui ; + if [[ "$?" == "0" ]]; then + exit 0 + fi + done + +fi \ No newline at end of file diff --git a/bin/armbian-interface b/bin/configng-tui similarity index 100% rename from bin/armbian-interface rename to bin/configng-tui From aeb37551de1a2a2809199d7f39efec961ed728c0 Mon Sep 17 00:00:00 2001 From: Tearran Date: Fri, 12 Apr 2024 01:35:35 -0700 Subject: [PATCH 47/48] Update README.md and related files with new screenshots and date --- README.md | 155 ++++++++++++++----------- bin/README.md | 18 ++- bin/armbian-configng | 13 ++- lib/armbian-configng/config.ng.docs.sh | 30 +++-- share/doc/armbian-configng/Home.md | 18 ++- share/doc/armbian-configng/system.gif | Bin 0 -> 222489 bytes 6 files changed, 146 insertions(+), 88 deletions(-) create mode 100644 share/doc/armbian-configng/system.gif diff --git a/README.md b/README.md index b3e4cb1d5..413391cff 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,100 @@ -

- Armbian logo -
- Armbian ConfigNG -
- CodeFactor -

- -# User guide +# Armbian configuration utility +Utility for configuring your board, divided into four main sections: + +- System - system and security settings, +- Network - wired, wireless, Bluetooth, access point, +- Personal - timezone, language, hostname, +- Software - system and 3rd party software install. + + + +To Configure and change global sytem settings, run the following command: `./armbian-configng` + +*** +## Screenshots +![edit-boot-env-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/448f0515-0854-4a8a-8421-53c8b72bb5c5) +![BT-connect-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/fef037ce-346d-4d70-9025-90f69fbdf5d3) +Following was updated on: +Fri Apr 12 01:33:08 AM MST 2024. + +*** +- ## **System** + - **S01** - Description: Enable Armbina kernal upgrades + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s01) + - **S02** - Description: Disable Armbina kernal upgrades + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s02) + - **S03** - Description: Edit the boot enviroment (WIP) + - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s03) + + +- ## **Network** + - **BT0** - Description: Install Bluetooth support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt0) + - **BT1** - Description: Remove Bluetooth support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt1) + - **BT3** - Description: Bluetooth Discover + - Status: [review](https://github.com/armbian/configng/wiki/Menu#bt3) + - **IR0** - Description: Install Infrared support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#ir0) + - **IR1** - Description: Uninstall Infrared support + - Status: [review](https://github.com/armbian/configng/wiki/Menu#ir1) + - **N00** - Description: Manage wifi network connections + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n00) + - **N01** - Description: Advanced Edit /etc/network/interface + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n01) + - **N02** - Description: Disconect and forget all wifi connections (Advanced) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n02) + - **N03** - Description: Toggle system IPv6/IPv4 internet protical + - Status: [review](https://github.com/armbian/configng/wiki/Menu#n03) + + +- ## **Localisation** + - **L00** - Description: Change Globla timezone (WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l00) + - **L01** - Description: Change Locales reconfigure the language and charitorset + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l01) + - **L02** - Description: Change Keyboard layout + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l02) + - **L03** - Description: Change APT mirrors + - Status: [review](https://github.com/armbian/configng/wiki/Menu#l03) + + +- ## **Software** + - **I00** - Description: Update Application Repository + - Status: [review](https://github.com/armbian/configng/wiki/Menu#i00) + - **I01** - Description: CLI System Monitor + - Status: [review](https://github.com/armbian/configng/wiki/Menu#i01) + + +- ## **Help** + - **H00** - Description: About This systme. (WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#h00) + - **H02** - Description: List of Config function(WIP) + - Status: [review](https://github.com/armbian/configng/wiki/Menu#h02) + + +*** ## Quick start Run the following commands: - echo "deb [signed-by=/usr/share/keyrings/armbian.gpg] https://armbian.github.io/configng stable main" \ - | sudo tee /etc/apt/sources.list.d/armbian-development.list > /dev/null + echo "deb [signed-by=/usr/share/keyrings/armbian.gpg] https://armbian.github.io/configng stable main" | sudo tee /etc/apt/sources.list.d/armbian-development.list > /dev/null armbian-configng --dev If all goes well you should see the Text-Based User Inerface (TUI) -### To see a list of all functions and their descriptions, run the following command: -~~~ -armbian-configng -h -~~~ -## Coding Style -follow the following coding style: +## Development +Development test brances are available for testing. To clone the development branch, run the following commands: + ~~~ -# @description A short description of the function. -# -# @exitcode 0 If successful. -# -# @options A description if there are options. -function group::string() {s - echo "hello world" - return 0 -} +git clone https://github.com/armbian/configng.git +cd configng ~~~ -## Codestyle can be used to auto generate - - [Markdown](share/armbian-configng/readme.md) - - [JSON](share/armbian-configng/data/armbian-configng.json) - - [CSV](share/armbian-configng/data/armbian-configng.csv) - - [HTML](share/armbian-configng/armbian-configng-table.html) - - [github.io](//tearran/github.io/armbian-configng/index.html) -## Functions list as of 2023-12-06 -## network -System and Security - -### set_wifi.sh - - - **Group Name:** network - - **Action Name:** NMTUI - - **Options:** none. - - **Description:** Network Manager. - -## system -Network Wired wireless Bluetooth access point - -### armbian_install.sh - - - **Group Name:** system - - **Action Name:** Install - - **Options:** none - - **Description:** Armbian installer. - -### hello_world.sh - - - **Group Name:** system - - **Action Name:** Hello - - **Options:** none - - **Description:** Hello System. - -### see_monitor.sh - - - **Group Name:** monitor - - **Action Name:** Benchmarking - - **Options:** - - **Description:** Armbian Monitor and Benchmarking. -# Included projects -- [Bash Utility](https://labbots.github.io/bash-utility) -- [Armbian config](https://github.com/armbian/config.git) +## Note: +> +> The Bash procedures embedded within the JSON structure are meticulously designed with a focus on clear naming conventions and the simplicity of key pairs. These procedures serve multiple purposes, including facilitating the generation of content in various formats, such as Whiptail, Markdown, json out and others. Moreover, they are utilized for evaluation and execution of commands outlined in the JSON structure. +> diff --git a/bin/README.md b/bin/README.md index 1e772cd04..413391cff 100644 --- a/bin/README.md +++ b/bin/README.md @@ -12,9 +12,11 @@ Utility for configuring your board, divided into four main sections: To Configure and change global sytem settings, run the following command: `./armbian-configng` *** - +## Screenshots +![edit-boot-env-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/448f0515-0854-4a8a-8421-53c8b72bb5c5) +![BT-connect-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/fef037ce-346d-4d70-9025-90f69fbdf5d3) Following was updated on: -Thu Apr 11 02:23:43 AM MST 2024. +Fri Apr 12 01:33:08 AM MST 2024. *** - ## **System** @@ -73,9 +75,17 @@ Thu Apr 11 02:23:43 AM MST 2024. *** +## Quick start +Run the following commands: + + echo "deb [signed-by=/usr/share/keyrings/armbian.gpg] https://armbian.github.io/configng stable main" | sudo tee /etc/apt/sources.list.d/armbian-development.list > /dev/null + + armbian-configng --dev + +If all goes well you should see the Text-Based User Inerface (TUI) -# Development -get the lastet version of the utility by running the following command: +## Development +Development test brances are available for testing. To clone the development branch, run the following commands: ~~~ git clone https://github.com/armbian/configng.git diff --git a/bin/armbian-configng b/bin/armbian-configng index 1ae72391c..c6d040f14 100644 --- a/bin/armbian-configng +++ b/bin/armbian-configng @@ -10,6 +10,13 @@ tput init # allows CTRL c to exit trap "exit" INT TERM +if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then +# TODO : Add help message + echo -e "\nComming soon:\nsimple help meassage\n" + exit 0 ; +fi + + # Get the script directory script_dir="$(dirname "$0")" @@ -25,9 +32,7 @@ json_data=$(cat "$json_file") # -# We're checking if the 'whiptail' command is available on the system. # 'whiptail' is a simple dialog box utility that works well with Bash. It doesn't have all the features of some other dialog box utilities, but it does everything we need for this script. -# If 'whiptail' is available, we assign its name to the variable DIALOG. This way, we can easily switch to another dialog box utility in the future if we need to, just by changing this one line of code. [[ -x "$(command -v whiptail)" ]] && DIALOG="whiptail" @@ -67,7 +72,11 @@ Runtime conditions..." | show_infobox ; fi + + + tput clear + # # Generate the top menu with the modified Object data while generate_top_menu "$json_data"; do tput clear ; done diff --git a/lib/armbian-configng/config.ng.docs.sh b/lib/armbian-configng/config.ng.docs.sh index 104900d71..791ba27e8 100644 --- a/lib/armbian-configng/config.ng.docs.sh +++ b/lib/armbian-configng/config.ng.docs.sh @@ -23,19 +23,20 @@ function generate_readme() { # [[ ! -f "$script_dir/images/logo.svg" ]] && generate_svg > "$script_dir/images/logo.svg" ; +echo "Sorting data\nUpdating documentation" | show_infobox ; +###################################### +# Generate the README.md file echo "$(see_jobs_list)" > "$script_dir/README.md" - - - - echo "Documents have been updated." | show_infobox +echo "Updating Readme.md" | show_infobox ###################################### -echo "Updating WIKI Menu" | show_infobox -cp "$script_dir/README.md" "$doc_dir/Home.md" +cp "$script_dir/README.md" "$doc_dir/Home.md" +cd "$script_dir" && cp ./README.md "../README.md" +echo "README.md has been updated." | show_infobox ###################################### @@ -284,7 +285,9 @@ Utility for configuring your board, divided into four main sections: To Configure and change global sytem settings, run the following command: \`./armbian-configng\` *** - +## Screenshots +![edit-boot-env-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/448f0515-0854-4a8a-8421-53c8b72bb5c5) +![BT-connect-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/fef037ce-346d-4d70-9025-90f69fbdf5d3) Following was updated on: $current_date. @@ -328,9 +331,18 @@ EOF cat << EOF *** +## Quick start +Run the following commands: + + echo "deb [signed-by=/usr/share/keyrings/armbian.gpg] https://armbian.github.io/configng stable main" \ + | sudo tee /etc/apt/sources.list.d/armbian-development.list > /dev/null + + armbian-configng --dev + +If all goes well you should see the Text-Based User Inerface (TUI) -# Development -get the lastet version of the utility by running the following command: +## Development +Development test brances are available for testing. To clone the development branch, run the following commands: ~~~ git clone https://github.com/armbian/configng.git diff --git a/share/doc/armbian-configng/Home.md b/share/doc/armbian-configng/Home.md index 1e772cd04..413391cff 100644 --- a/share/doc/armbian-configng/Home.md +++ b/share/doc/armbian-configng/Home.md @@ -12,9 +12,11 @@ Utility for configuring your board, divided into four main sections: To Configure and change global sytem settings, run the following command: `./armbian-configng` *** - +## Screenshots +![edit-boot-env-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/448f0515-0854-4a8a-8421-53c8b72bb5c5) +![BT-connect-2024-04-03 10-06-58](https://github.com/armbian/configng/assets/2831630/fef037ce-346d-4d70-9025-90f69fbdf5d3) Following was updated on: -Thu Apr 11 02:23:43 AM MST 2024. +Fri Apr 12 01:33:08 AM MST 2024. *** - ## **System** @@ -73,9 +75,17 @@ Thu Apr 11 02:23:43 AM MST 2024. *** +## Quick start +Run the following commands: + + echo "deb [signed-by=/usr/share/keyrings/armbian.gpg] https://armbian.github.io/configng stable main" | sudo tee /etc/apt/sources.list.d/armbian-development.list > /dev/null + + armbian-configng --dev + +If all goes well you should see the Text-Based User Inerface (TUI) -# Development -get the lastet version of the utility by running the following command: +## Development +Development test brances are available for testing. To clone the development branch, run the following commands: ~~~ git clone https://github.com/armbian/configng.git diff --git a/share/doc/armbian-configng/system.gif b/share/doc/armbian-configng/system.gif new file mode 100644 index 0000000000000000000000000000000000000000..2e839d1157eadbc8db3277cc5c0b17b502a2fb09 GIT binary patch literal 222489 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,u1udl|6 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

_w6>-UeZ1ii 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Ɍ^-I>qv7baP06o0(@BR9&n8@rv*OtZur74;J5|1In%rbcjetVl2 zRBQqz-@mbc=Hb)+vOetP`J_<6K4$7BCFbY1A@U9}H}T21$dH?t7_Z_G$D?MU-64T*G-&weAU=_+dZ^W)N&N05x8rt80#w>VaBUU)|yDmZT%SoHVH zR^|=ua*XQ39X>mgQJP6I=m+BI>@%|8p8*=zH|(=mgpMp{ZLw{G9(rt)*O*-vts|Iv zTB8fCcXQ90q`p|lkJq|5la<~*^3#=NG;8C^fxRbn)WhuGata5cAaQ2n=UojoN2pN- zLw(l$qnU1bh#jLkwb_3FCt*F2=0gUoBm`s(2@ z;D#a1E#;Zt0!MtuNWF)o^0{h`xJ@4~?STl2_2-DAuIvFVjwGao#QN9KmQrrf8z%{8+xvMK-PS>@c>-NZbWJw?xmLTL@t2F zUN*4`&?Pl0lDT-VGo$eQ(QQJpLGh{F=svbj+*G%3R2t)2}t_Qz|)OQJs>^xMVCyNG- z68@HQH21Jbqg3w^g}(@A9;e-iB*%MCHubc~;?1hWI z@VUw@mde9G;E1Xsqy(P7*^rHWw48A{=d+y>08?K*59>=Le+-(o%CZtfsLnF+!YH0@ z20%hvY0E9g@2f`k=pAI!p~l%7s>qkt4`PnwIJC)r4WO$zPAt!(SpCxf5Ho zT%I>gHmE1k*>?>=`e!+jr0uok+D!47re>E0^f6DL@fuTCDF7=V&5_7tq}rH} zdSFAQO_qQL1_n-AbDDE5g){s3*Vs8_W-~6Yqdq2TFYl6#sk|a06iRf3qH%n3UFF?= z?eNo33IiuWMxXrGsi^1OOQc$5rxJZgH+U?vV>`s{Sa&wsbHam<#+`iSP1M>qy~FMu4g+R zEAL}6hOVs!O}`3w@s2&~Lc~87K~`_E2nF06)5_u}2vwF0p@fOO(%oqYfW2hDt5A3& zGr>Szs=62K(fF{~Raxfi=lp!1$2HrLAN+Ir8uX@Zc8Uknp-MhKRSbJ|{ocHL_sSN= z5k_4X<7rq&xHKXwr`ln!mXTr-i+hiLSD ziYb5i@LaF7+DqUx`Yy6#SGfesV>HtSUz5Gi9;h}LfOjRH7uL5=Hd^=!KJ3abcKc@k zv7(2`@FVv98>W&KVW%>dRK7cWrOtMLp#}a{U7RY%b!|_ZH=Mkt?ZLBlF-_aLS#|<& zF?3)VL-w^03+DTcT_!VGQnhsWwM3Mr7qb;AI^j^8t)K<~;~AA!Iy-CAQ<*#()E#V^ zMMs^?+?6jD!~?04?|Qz?FuCPJx3t|Z2P12}VqAA3J-P@bgmRlTuWJ>N|8F+5-fPK`X zvj!S4iFdKiIQP{H(0yH1V%whCf#B)7n`#$td3O6#u22sTXZeUrv&;5;Pg914-1&Fp zwH;Ti|0?h(Te1^P;LUPRbA_3(ebKMva1n z>8DGLZ8|-jsR7iRevm#3bGxy@oZ8gZ0OGUVDDDP;t}+OZ-gTx|Avtg83FW7X5~j;U zZg6F1JvG8?h!wH%WUwQn6n0-3Rio(jK6REU5XLLp=l_tT2|MX`atg@--WJMQ<+4a@ zT3>+)poa~wMxm8xplQ9mm8DdpGG?1n1D)9{g}S!sbX-Ajyp*=;E%ZhmmKeCN?&CS2 ztN9>)`PI>=K#%N2Uj~YNHsG`yzlOFa^G8$VGY+i zp(iKLq`?Bmy8(8FL7v4afI<&grQawsNa&;)@(fDf1J}FH+=emy!6~xofjj8OPnttT z;Nz1rshKjthzG!*_s}K_oc}w#88KWIXM_Fjs$0plcHSc0pfB4FIdd5pPp~%eNOufK z-!tWkZikk>J+OT(ef#)+c`!`jUHo&Ww4DM-U~@)LbB4!SMq#Nusxu1fndtW(Elt$k zzalxrhd=g6JV5jY+TCTD(7kJrq%}woDl@sru!0UU`U#ku9)o^If>}qQZxCld==RA_ zsPH@N%y3rz+SVsrz&J5c7;grBlK>G=ggmUQ$+msugo^<=DHAZ{fQrS>V-jGGTB0U~ zY0_qz)B6LmXBmbyNz;qUX)Q9eC4}1eIqaaHDoq4oh+A)l!V{kbTk`fV4S~|xAl+XH zdrQ2YB%NDs*pBuLJltcT4uqC^L-1M|vE^DBX8Ix$^U+Adi{84s17xh$lnWitkMy41 zKLx?VLQFv(>X!hVG5q9^wl^OXfU>p;J@+y%8DJj6F-?|c{3!t!20owV_HHCn!u;`RDE=kM; zdsRE{1Wh?l8yHsuJ`#bij(G8qBT~10gs?={DxkU-6B(~PWr@-H0E~BLWAh-%MDJSQ zlF%4(U*N4z#-E_W>O@O3QBGn4$f~;d8;cnNh-=(q5CI5d4%Z?=F~G5t<>>nWrMwZ? z^8xT2(7wkt62AybNA0(X-$OpR<6J)K3UEXUYAeNzQc{h2^d4vfo5uklA#m`039u`R zj1Dou!M8&xSyT(aKmHV?L2O3z;GO{sF$$6uVOw8>82~JV#OqL;Z#5SOW=WQa z=X4)~got&;BX@Jmn?2$oOm@04qRSFkIF11?lOaMdOnKXtJ8y45+s~oFYGa5TNC=#; z6}YR3`%`lGdkawB0z5TGs(Gq^dT)Z;1Q`Byaxi_3?WU>?1fgD`Jk^AJ~6<0J*JaXyU9$)Aq4*=z#luK*!r&;t%O zXbyN@YS-q47D;!uW!)4Kk!U8e?7Qe@SV_nLP;`79H^+Lb*ogsC4CPu+K9I^g7RxXp zOq8GnC96LqgX@uJ_{D9PEl)Fyr`5y`6M=t@NP8_HnCPRV3rPG#WJVmr9TV%%-zp@E zk$JkGewKDhm&Pp11HNUd(9;8G%x(CeJpB($qrY!NQanQueboHY5jCFP zWKtybh$p1PC;~-+wr|-et@Tke#JE&ed(%H%au!|z7P^??4ZfdZ9Da$0+Om}yPxo9P zG%W}Bxv36v1Z1`b7!ti6{@nfdw~idy2+05~RUaM;hpH_Xp-T)zFx6$+IVf&BB0diz zfS`zG?_%y$|DaVJvo^a5+#J8-D9Dw}W2u)of|}2Y3-*ucdabVAkvol>*y^D_QYrP= zHh(U8G}Oqp(f6b9n88P>21o-dsi+!43VA4X_cFN2Xzw;do@LLJv&-8It(qkzAc!E8ZtroGl~@`$dqOAt8d(@$hWM;v`|%qX&+8d zjkUkA)EBykq^oN8134E8@Qk0VUId>~xrVpuv${@_Y9_>?*s6&BH{4 zKn169eH_fwbhxQKcW$LQfA7i1bo82swKu9l8uv?G$?+_1e|g`b*>BVo zeD0c6iLhI(3vMIz2AZK}^Zo!%+wtJXMvMN(r*TIb<&S{aMF9M7+zw+$TYiJANoByb zCn;&rTyHDVvbCZPu=w+Fng3~KQ{9dMXG4aSL_=NM%2Nqa1iIZOjeOEFJ`}=4U#@l; zPDk1D)9}(nuY(TKd5CND2Nhn4GCj}(juz(24_bPmxLl}}aVMzOn!;!4>H~$#PsG6b z-`=@c1zi_%^ey}RTmZV6IT+*NDOGY>4rA-_QU4JnP6BMCpyvqq;qo|Jdfy(_@bEXZ zDHIhCk67#eT4~#{Xt8-LR!VUE3mFSeP{Vz{AEOW0y#S6>n=k#6d|1^eCb*k41+aw< z(8Ouj^&)-Ghbcl)8;t1D^GA~WX?r8_DV;6`;}$@`(by8bIO_y2;NTJ0Z5yFQO!9>% zGH@j0#XvnIsRcM7Duy#&6wL#nS9U{i`P8Xnk|nf3#93%Nq(;B+4n>t4uT(!Eo`Q6g zTY&hgKlu4q@(;-#LpizpvkqE<&LVo4p>cWFU!3{zxW5#UvTHXcB(7^)FQmN$fEe8s zSj7Nk;-sxDJRms;2PJxMcBNiPJCwfhcpW0PSNhsoH$>zIkdo@SqK8BB@5;$K?)D%- zXgU%{kZRu%WdoqmagaYJY1(z0(nqA6HQ@6IXbtJpxr(EB_*3|UXrZ7KR{|p$TU<1K zg)Y%iBUvwdB}r4d2qpbklLQU~_sV0iFs)sNX>yzxK{vj5fbStMfZm*7?!m#7^??_$ z7xT2eIvs`-^C0#u0T`15QVp=f9tcMVoy-?Q$!U=N-2)yAzPa~+jjgznUw|zD-H{-x zRO#5(!IR5_ELqFY_y*JEP*WoGzT?B9pT1?KdLw%8_uum!34J@&FZHr$UyDxSOAHg|n&u6T2ft2|$7J6|3=Uy(9jSv+6eHeWk7U$;45ue{J`yU-N8 z@E~QOxp?7G+d}KuLfhs7PkFJ!cCjmX@mb1ZZ}H-bw#B}&#aEk)1Ik|qZNCl$e;rQw zI#T@gW82r!v9DvBU-`;Q>gH-j28_R?J%g{H=Vn+4^o8z{TIApA|F>O~pQ{KjjNZ2adN^THZ(gfZ4M;S^ zZPa|)MK&nnF7uvbq8r3w8Tk8jw96%>6JT`D5lQ#b6iEu|@RidN-(6at_)&!Iw40+@ zIsml+*#A;O;U+Lp3n*1$zWSeoBB&RZIHIF-iKXF}I?RPA;VPa)!NH6s%h{ootesOMF4oX)aQS>+L z$0@*nd5pM{#O+}FM|g9P?x%SgaF&eruVON#O<~aZ=Qnpov5=9hkksDuwgvW3T8FfyQBx-HXwStzvJ#*^<*eN}f#T&(rOp0%f^@53Zmzt3^;OkZSi z=z`1UQcS(S>p&Num%*E|YuT@)VrX%gUaiDC_T*|%qbN*8?p z3NHp_YJS;dhTlkKbEi}uztBS&K70h}=87c&D6IdcOAK)jiaS@sDcl*Q=pv(9&RwM{ zMx9HI=a*y-M`7x2)e*4SH&Xe3dzb_(=uYCS)H=)YVOPF(`q`ng7_IGytK}LKn4IOC zgM{R~5=YohBb}i6ADVfovjn27jxw=kNb!tD;T;5rRIArzv=AO(_W(VsO zb&Z>u+-xz}4GVtxjJeK=@XKV`d{xOsLL4urN)(Ob3#E5v;{Djo<|KExREeJPp+8B1 z%$%4SgGvalTu9^+WN$?(AkU60=jFVNXU6MU7~C@?k?^4$09%F4eUxywhaf6k<) zlPWXYse$!eyliNlM!HhQ>dh8eQ!%u`$Ad&PGs4u_Iy<1YkmP%JrwdU=yYV#l0IFjb zw@esfe)h)tQWaKjQ?Q|e8^tFz>5W-L<(hR_pz2Nv|4<$gk#8yUsr-mu$4E>L$@4w3 zah#<+hzdh@WuDf3#T;okGTs7K{Hvl;@KmW@N@*)fOv&^pC0lPJbWaXC{jl?&RNZf_ z8W(8IjAJ<1IP=F#VMzN}45aFqYah|N5-jFCkcX?MQ^X1EeX$(v43l)&4x;kzgr66r zT)9Hid*c+$%5SsF(e6aK`gXBk0E{CKz@-N+pdyGA#oTy=xCa>xl(HV9VkxxQy_A)# zwBc+l!-(gG#cQ*q`sj#`L{EflLNjxfkp->71}lK_=Z*E@vT)C2WHb7IXvZ*d-2F#^cc|q-TyAbbmt_x754|HX{V(y%# zh*Nurhh$+ac)Qz_c0$jJ#}Q(}A~<;S;nUPEBFWrYt#%;cPNsQ{rU>aMF{!OC5IhT5 zUJkd%Os7dgs#BtMhK9-ZG1^q^$Y8nTnT8iAO%L~neO?SrXc#PI@tGZh!$DVq<@-pt zRQAyOZuQruL-JAmOCi9-JiG1`dbw5gkhJR za5jLlK539FK}+dS&-OhY2&jfT6UUN7h^MQ!5o?T_M;qtR8u_fBa4#ZenXOlJ@)8j1 zOQNB8)XeYdfkUA*Ws~Ya1&@}ard^jhaauc2A354q1;l&J`9kt>PigbOZbjGbe5&l= zCEtQZjhf1IFv|06w`sZMmvHuWjZ!3{fq^OoE>N_6IbO9)dwn%A!BA4RedQY*358xTzab0a4se+Pvca;zy>JP7cP1fLO{F@L%u^}#;25A;Bq0$rI zAsJyYTu4@xhg4O+Lp6*hh8|BbsT@4+v=#74SiTBtr$!<>g0D_`5v!uyTQH_jwR&Cr z$s2GGYS0?);Y|dQzn*lz=gIV$5x>HK9hL2z>(NIYEw)zrUerN$_zQicOY&w- zev^2iu;t*N9;haY`RZ@S4o=Qp0IdcZ)lJhPdGPRq6EBSWa$G5f$ypO`y;%HWMVh##@ifvwS#zc zFDs%Xe=`4^V=u(SVYfUDCDAkPH2!?WF5E*iUHmuM;Z0dSvT=J_dVA!K^(;eB-uu+> z0J_CR=7nq-?&IUTuJ5@tO-@?wwhITIUnnnyhp^LfTkto&$Q7`7f_ydw- zQ&M1r4}_N+p9cHx^Bbi~o;eyYPLh8h5zl*-xz`shub@~DkJzbbA)p9@L+E?gwlb^X#=+SOsl)QG*T{i1E;i+${|r?z|{p1la$5O&FzN|lS>+(~W>#^OtZ~Q#i+GzoDmHbuA`SlXcQn~G7g=>^m z_GvR?pp|Q~n?SMR%4N8Xu(SQVZ2M~FQzBdcQ)@c#IK!%Z{jAu#KsBtZ+S(PcmqIaG z+gc81x9p9IQ)`!Qy19kYo=RwyZOze6XeSg9@To{f0nX{ww`f za{gaSjwKF$809VM@`ZFboZLz{y>}c&w$~h_)L^$C7(6ZpWx}=W~?npm+@QUr0H)Y z?xLOXSaCPTXEq1u37uCUq#kS&iKd=2TN+}NYurJ`s26;))9zavKcDi!!vhQb!|$}S zUgnpPO*bx1=Gj8r61 z=BBU3Khv4Hesnl`=0?(V)l6~nueq6<42141Hys}{Tk`3~vLu8ZeC5OL)KSOIfO9*m zI^8M&nDec8h#DY%y=~6~8J2E61A|?PnZFv1rM)B@)e5gp!_g(gg<9D6_=URGU-Juh zc?iA5`Y!yz#fDzZn#IOG%Z0^z1CDxMn}+-je!V{uQS8UZkAK|HcL9-iehvqn_)q$sW$$ByT53SEt-u_Se{cvf0p}xoP?_$&YLw~dvQY6dm@i%T2WcWg^L9gnv_IQaL>HX!TNs2dp*^!cKHurRcct4M_7)gtrhR9$0;% zQYSsrNMbP86qa9=3R>AmTYV|zs%9sAvValJ1_vK)zE4<=w>v<)0bP^={vZ4Mb5IZ{ z8T?mN^AduP{}lf}W|OFyGuoR+ym0;1RcH1)E`?0?Lr znb%zVv&wFdn&#eCdW&Y0NWlJH)XXV%d;H+*r{=`}&9ez>Im=H29RJ5`iV|cHA}R%$ zO2=mfS;S23akhG4)OfabMdf&o{)5@^v;QjrI~k!f$)Vw+C-a;&t0wbZE$1dLdi?(~ zo7kGwv!%I~^Rs0fN8P#d0>7BKTSXDo{|9^T;nd_CZi}W@$`^VkK^YhAJXzq=|wUngXH*=~hrwY(E2{q9QqQ@4L_5_nbX*&&-+I zX7(Szd}Q)weQQ1Mde(a1aQ@hb_ZPVtMjsmMO5#3TYHYxiVi+69o)-+({9ecDpUxIX zG*(g>Sycuz;uiMq5iifHDO145=3C1jjIS0OoZNqv|7!MQ>)40)A4m`0p7?Za^2v!$ zmrpg#8DH;xIP&gB+xMHU1l})OuM7H0a{u9&IY3;&DDi6$gra~%)vH^?@`z9((X^Tg z6!xM$*TX;fzrTQC%h=&UXDGGUvn6@~-AcYSE(OY4BF@JETD^r5J3=DMc=+sMm1t`d z_TzAGFN9=&CXr)7Pl| zag{WVdhb<{bJp(r9ana7jZfhs?@7IQ+LiqE!$EXz7`bcTkH550*snoYf8P4>ho(dF z8Ogu2(R5a6Ta8IV-|X-mLa`W{ckgsPqonOzR7M16LK%i0VrV|sh|)A>z%GRohTsI) znF)<;-8Nfs?kYt;-_ahI8?@(oU)c7ij;$eL)cqc6anJmF#A=KAX9fLrZz}+1>dLNIm7Ta(0*aCzzKh2 z;H=-lD!>>a*AmmoCUXK%zJI{1|5>e57Hs*`K^FHcpO{3yDT_3UauM54e6gk_pZo-x za7oJB^GV+$nG(!JvbSmBA_GoNU8{AiWGRGv50R&~D5~w}f1ha2X=?12gJk+KBlkV6qc!$WStwBBqV-nlM)gsC9 z&BoVmykD=q+r6rAl5bG3CQI%CO?xpUUm*?Vfr4um=LjPa+HhMG}J4V zB4pwvs!>}!`eZ7EECZivOyE$Tg4i@k%&nG)_UKo6BFwNEt(H1I)UWYNm`M^(Y(<$K z1KKJ}Y!9CrnbM&FJ=>)$v1wB7qQ{_7$WnIDXpO>+p}`F)OF2}DTBUm)cP%QGa$|gI zRi=mTTHjha6Q5hF_Riy;*fe=Id9+sJ$Iv~;UrTv3i8?&ebI4g`nZx#}(^ecFa<^U1 z=j7HA#HLBF5SbGO$>EI^445}BzR=6J*U2_3%WfOUvTV^+cUz~YfVEsC@`zG*^BmbG z7=yR9xf%s--XhPpD5?LXYjS+Et!naUX=7XcVjO7X?9l>zFRT*ltcyK6W>M0#_DpYj z-vi&2mZS$0N|=mGqk^qpVe0pzEFT0jfx5)9#>7Fr^?AuNTGeiKd*70qaI(s_p+2Tf zt=PL3Cp?Ggee&v9)W5%uH9$%reTdV)%IiO}TL0HL`K<93zAN8gM^orU+J8}`PM=HHI~jkQFD@5UN69LE`Udozy%PoD1ta0npj@<&JxM`9Rm^f z^`o!oad0txVtIY>J@KL`tL7C=$_C{QaC5Cg%DG&D4BGV4+H1%jSrm&!3b7GW*dv2P zx^4>9JX5#TW9gA0x8p-okok&eE>mMzIF_NCD$$ama_##E;~m{u08=7ulzd+;wT+fd z+Pn7QQ>z2nfFZ3#TlP>%HQ9yHIsHd=)IgA7eAT-=8-&@=X7JS$99s2lJ6iXQSYB%t z(>x4UHT?R5SH?bmxuf=t!jK@_5Xq<|22w1qUE~fV8w0{Ek%mg#o&8GDqa3VASiOtP ziV@3e$kEwhr*j;!yoOj&DIk-3Yu7g1AO074{V7HgGSB_`=ur<>fVBtx|4&~37f${w zPA(Uejn?UH8Xn%3vRufOs5jW+IpS9#HkbR<8wC!J1l(FK=H=F#M0?&3d9qy6Ia


i)YIyc@XQe0pC75m?R{@7C_d+x8;4=Jr%q^r=8Rh;)2_j*A+ICpo)wKp6W_K2(}U-zR32+hR&22zc`?G7~=mM3jht+>>QF zWl)K77?@+22nHCgCe{eU$Yeu6469ewxLztLYg#DPXbJHkvi4}PGayU#&5>!VqeDzu zDB?k6sr;sX2_3>KNdu>~)+|EgT|@mn+7rS)IC3Z+lxo5wiU*N*-}IxyXyJ{Dwi7hw zDY{tf@3i(24x4s@c4R}19K>ROtJ{-dtJJnJ5A^N?vDkOX0$8X@ymq=~mYld8TpRL0 zn-kLbw&|mHz%Kxzmlm{y)GPtoa?EI-n?AMZURUhXzcottHf)2L4?2NXdOvkM%OE%# zT&l~@)@>Ud`+Rfph-ELKZ8bbc*K`r$eQRY3n~yU0pB+F%edO2no+~51g%vQfw)sbt87aMU8kv1?ID}Ezmqg#x8@ny3 zDY0^1ZlCRNu<`cOO|n%3&l*U}%A+T)wqAfAy=d+$e6+1i_Wa`Dk-ryCIqhms7GLC8HhOWL_RnhnWty3SDzD0sf)@_- zK+*Hs%*hY^&4gZeIq&x7;4`0QdVe2p0Y4gP@Ok&_o>mwXYE=T8n4y}++*)xYsIuMhZ@ZD}-hXBu)wOD;0Mijdwa zkuA~)O_;g84KnSp=jDXro3#BuMt&vXq>r%9&3u%8zj#zH2uWVXY2UBc2lSEG``9Vc z>LS|wqiH* zD6tyID~&rT{=R6Ir1cYzTFORvcr@ULUYe!koHW1rwy~zf>%*mnhU^bbO&ud2E?>PX z!hCFQnezI0rQ>bNE?3#bxxfp;$#05#Qvd9ncCgbfYUYmil~2}ad~5&Y)ZcMQz?*b_ z`l&;ID+P-6Tx(^KylrrUov#$yTsytF-iPGo*I&Cj|FbT8jpXS%KhX2m*8FPE=aTzh z_%KCrp5}kN$gz*_T0?{`4MKkIH}6&c>itiT+QDGatK=%m{Qug{64sjkmG!fz@`AIhHJ8YPO?f1G$`VyP#SAx8SgAG$;G#kOi(NE zF>xAI+hfsr&UYtm?gP_92eC>EFzDr!6W)T?mL5`9Wl?uW~E&4&~L z{N|tUul2S9Fs%DpKXX!q&u2mHQAx{AJ6N}!owZkq8{RhIwE#&oIm=$S$iw%H9>?iw z|7X-J=<(;yuf%={*;zjGpLWH(#~|^p=w>}hS8Yl%Bwd@M;;mftzzMZBKkh^xG8Js< zR4U5O5YZAixGOgft%()6Xih>G2EH#Z)xhvxg;XwWSAZu34tGOl2l3YW=hFDw7|H%euvn4jVx?1r(xe%8Fx-$yc8mrVJjwWB1GZ5vjm8+lqsYUI z7|an6bJd9XSCOZ`Vj^;z@I1=nbrDsuFYOec5J-WTgpV#_x5a37}198u2E=Ui57|106cA8L_rSwS@!WDznm_+rjZFpf1EJ1rtkT zOCF#=8A(fGMZVzXN*W@D;Sm;sN{BnC`{gCGxPTorEQ5D0>;XT{^gQ@QO@@}{3XNIz zb>3+oh3xj1Y5K6GBe>9!pP)p@68T4p(2m|c2PZF9kwQtZ`@xdQX4I&cXO)>6f)?b` z>jftprTb$1;#L5WL3+p_eG7I{qOpx*5)1}?#XiDONh9f@g&7l(v!~_-Y zyW<>*b6oTw_1BKfZj&K7cP?#HgAc-jTk+GcfrAYNfhCEw4^k11-C5BL(P*Rl?7gjU z{|MD7!y1x8I4w=0Um@IP)*4{Im%7QDuXhf5-O=X=Pe7%5e&&-xZjM#`@^=dO$}rtW zWy!iYLAo_8d>lS>GO>M@$7>)sIU=y#CCXM+y-cljIX{+pz ze?El~!WL?#amUCCy(pDWHAf(xmq~N{Y+nLQ4p|lliw{p{TGCKVwS{2C`!g+UP_h)C zMJ`LwrzgvUA3lAt;c7I%RA8kACf9B-*|~Z19~T5l&I2Dc@1nYzeeukhD+i9&*r|vj zB~VfnNDqg?H03w5mGD>KV4mzVXYZT?N?pjB2m*ZqDY*DZv-vF zk}je_x6a-TdU(XWTON8NYV0PJXV8OTz;<{L(TSuXl4xwC;lz}v#<9qR(dP5cb{8Se zn0LiYyhnRiDe~m>&rA)@@$xkO;?}KOUj)HhiX34lK&3O;B0 z|9%3w-f`^2jnCOmkq`^EI1{2;a%<#qxpKfm!(Rv3;zNXg6+r|}ar4o1rQRA-ah}js z972c9VA0m^830t)Cyj+|5uy5_c9t1T108wf7^OffV(8|x8w||7j!Tzfmzwk=L9&Zr zXBQd^E8rt6g6I;XKl5H~6U3Q$KqNEvKTY$ANozCIG+mvhdA0m(^F&2U4LvZ^+-dd`RbX$~YknaUIg* zxB2HF%|8$z)4bted%b;!(B>y(z>%UkkPZNAcJ+`P^U|S#W;1a&2*A_nRK-94_p=iY$Nkmn1;L=uRYnC7f5(u4O#)cm-!_(yq057QBMgjUF59Y#QpyrGZC5A%i z(Dc1QI@Ph_Yfc-XouoeCN3v6A0DN$UiaA7*28!4zqx+3{A=6$*2-!e^oDxd5aNts$ zeYqLHej+T^`XCt$OBd+_$%$Y|Vc5+g_}M8nNCMoDx{Emn^WTU%2txD`052ix?NK0> zmZe6=W7`1(UJ@cfvW=|b_(O6J1*oB5ooJA~27pToKq4XzH)akf$;>8!H{zkX0`wCi z#*>dSOaf|Gr8|ydedm~pAh*;>jC7sT;a4#tesF3B!VreNGLC7$9kHBCeEd~9o{E!} zONDEp=L-!ke1-bBt7Y|Jd@=whJTO&l*TnNm*99)g2ax#zdvpP*Bl`Y{z~uN@dy*Yy zi36W!%8(`07vTARxy=oh<^V*Nghg>dtsIy~5THiDtT8u#weZ!6$7CG=ZY<`^#blR@ zR;9J>ZST_pkQ`V&Cv#*9bT=Fj|3avE?3!E(5(i&p0!@UXn~{JxV8Ex$p0{}R6&bW6 z9e^bRg0BZL;cA`}&KGGQ@++VN0KswSjKzHUdr&C_*exnl5|0f%3S8kj6~2Qj5`B4l zG2tZS0iiUPkBO&Y(*RU+c$pOeNE4uMB%lz}~@0TrCB z$fXigZ>lGy5DShZ5uu(uIfKQ`65ph6MFCIf%1x8NK^~S^5OC#n;3+{NO3SAdgk7@) z2XOP_hvUpB&~tJa3-0+gBB2G3-ADsL7@MSsh%-OoWV}O?h*EqY5qX1x-dgVu#jDiN zllD{mR>zOISLY}1g%_<#+x(QiPCWoF-8xDEnup0BWF5~01I4=HbS}>~>DU?&M!X)_ zACEGagKOz3(K@hNR*)2UpooTQzKOXr1XKiA7bzVyDgw^XL1P^-->=8;ZP>f!oYcwl zNqPZM1Lc@GkSr<7R#+V4N+)pKUaUa0xFADXZpmJtkYHmsvdO0bo=6D2!)j2B-CUY@DE&v6 zF3(C8;q2;f4c9tNatA)V1`$B^!7j8>d8>HQFKHYRnp$K(SO-e~deO2X%9;y_CL+(@ zLOhfYw}|&D-sE932QlGV-;sk_15hg}v=?b_f=_;0cY-4av*2WeHy-F)h&exQd#Cus zkKz;W-q}S(r^+?jZDOD3$pn1aC#4YX7F=kXYnWYqM7kVwTp>LiW*z>usRd)-R=4}n z2kL>TCL2NW(_ePdN~fNtL!ZIzgIxoze1MFX1P#|ggIBx{l=!Uu@_saH3&Wi#{k1#K z3S3LWwu5TF$orsl!y5zXSKcM(Z92&Rn#`2j4#8Qu&V>#RKn0%mJ}R(bOM8~3IDOL| zUL9|iZ1)uAypeYLvsHrKn-IU~xNMPb0&qM|_8!#UyA`K@RQ8_Tx#MvzIR{5_7qS*N0 z)_BCKtj0D@=I03D`hM50S#l+wjMCb-oqt-Z%uXw8f}G~x~cfA|4}#;VJF$e zm1yLbi}0sS+jdJ)LG?@saMUFuq%aL+hZKMS#B~xEG`k;gwS=ZMU2W74*Sx<^DJSJ> zQ;XEaX2GJLVSYgtC;YMmkSVN7(ofj*tlts;8hRib%}^1erY z5ww)#HIV4?dKuzN^Y$eAHcfbMs&ic15@zUjG`P;P^Hxh-x2&ELu2mW5=nC`tN~@9x zyZs6x@Q0w8+4MiODxSpbzSkH{efi6T_b1WS1U5Bkt%x1S*1H1WtfL78*>1 zC<;(lNs^Bf9avnbeHk|JO3;*pWP4i#GiX45oSTm zxCO@${DH=N#K9X9e_R+}kHCu!fsQ5Q8jF#x2k0IIqIq)9=)eIyTuMZRx?M^}g7H5J z038y_6Q)wN6ENUfTAhIqosk{_%p@K-N7I6kH7nPo14vkNn!j~HpnjWJPC_Ml)fzEC zB@r8)Gz!5PYP+4VY0i+Iq8v-_m9u+O=%i+7;eA^?u0AX2CvD9-InQ=kk$Xw4xxG>sq-CXc%-&v0{3 zLQX0NwJ!|t5}*pc+4@(#0Va-Wl@%lMQFB8Iknpbi6@b1@(#n$b24O}Rw_|(-d48^R z4^?%(#{aNu+&6@M-I>GiC8Qy78xo8{YQ;mA0`8Qk&Fwlpu>m;pD?CNYJ5SzPmIk>f z2Sv`MxdK);f9m#TJ1BbEYZ@e>co1_g2ugxSQUpnsS)evhfa0uAja;M&B5?R^9@VZrJheK>(C~o2JAVasGwtUpn zFD?{gQcWKLwPo{&rDWeLfLsIa@%a`M{q6?Q4&g)VWp$2_t1`#e3wmOXTsVi*`_+j)pVkSC;zR z&ZL=`^{|w8y!ENDIv6&QzvJH4Du>hSFctqrN{!nY4Yvya_m8VCZs^nYFr2!hi9|oX zHOG|kk+JUh^L6R)gp>2eSjkh>@5k?uMJAH2%U|-Y(ZhEW2hRn53F|riXC1Bxv~+FE2?-76iPvlLtLS%I2<5-bHjq^@QiM7*YMzfXTc7XjDkN?% zZ517SdAD-ck3%nfeAjZcTr!yG4Jn+}d)f!s1J{k}{{|uG zHRJ3{QK-pza4Pjz*NrHhiP$Pr}FqG2#1t`Xr zPfLS!bMTckziO`1XPp)goxI2<#0Rg{Xt*-M`4D#`X|WonhtB3$Du$B4gSa4om#Gxf zxqLzKaZd9vKDyw_z{@8e(Yd8Poc&k6g|e^9+C=_(AnnW`<>PU`1d-VlHg)esxfEw* z56(8stw_8!(bxl~X7DWwsTpBF&Qu|~S@`>I(Uot|fYmiSLDqX=J162!U1E_6d7m`p zV+`K6FBkmI4A3}GFyGuS2NFW$RO9DBzK33Y(zL6kUgma{nGEU$suaB+E<3>ery&Yi z^ZyEMQy!QPYt2+OY_zS2xD(%+>vZ)f15(L3OY*{g*x+KcT8+9 zI(tDk7wjI3hoQuVE_I|)xqeXIUHilbc}?Ac8%4pdHJ#qBMla_eRY(yx5l4RL@zItsv34Ka^ZoS&X7uLQ7mN|)bbV;gAgisCYszl5EbH<(x-;q7Yi${qGOez(8UFG` zIXRm(&1@ti5#z4EsZGz}S-VaVR_Qzmv(q}$DfMTpt|HmGpjVI^Z89lA+{jQ#f5SL>2`3%3k*>kd z@-qVjuo_EQE-UKqACtHtGhmoS(R7N zdoFe60_;uxG0HU#P+6jR7)h$NrD?q)Bb0W=mFSEQZ-g>hw za{N9;VZ7|7VZC8yAGCCUyURR5ncfyubY5+TfvxDL?`ZSQt%oX>+@8atZ3Z`QJ5n-! z{++A7ZyfJoQpb4Z7yG@rsDX#VQ?gY*2BIB3-L{|pK7K*eChhPOO<^cZaGAJ9Z$t_u z!+N56gI|oRrmb&wz=|HaJ;vRtVl4g8M6D(~*2B%#ub^b2PTwuoYv(P$l8%Xbv-DV> zDBB$sQxgrF24lA!y|tt2`@}^jcpN3&*1tw!vT=)BoL})R{|4*HOWV`q{OfIZHU&&J z1rEjqwBOo!_1NU)J@EZOgSG)JC6mq3Zu>(fZv}L8OkO#hzCZMhZQ!k`$*ac)_lJGE z71;BA@>-fWt{H9@)Tc1T%XB*sA>S2r*Lv!DUitx1q^@1?NWfG}>EMBVR$ajlk4=g1 z&<@79*@ZkVnQFV}b}(*dSIAVyRQt8`g9oDQc0HMz>bNm@@X*n&T`#^*-M9lk6rXMv z`buHC^PbzGBgI{zZ>^_qK1x57RByNYL%{T{>A^$C+PilDb8NcnCH!#mpk3H<$#nNS zx5FuuU18rlrf+{qKYZ$q-JV}l(>*^152t?X+Oz(B`i=-5PlMZ2A&N76qwampVSvuZc)tyV*y`l+Fz3yC-Dt?2b^lF*9hEk?^-n zNo9KG?xwp5XODL8)%Y=U&k1palWrfWtvEZh#r;S@v8X#zZ`17X_KYJ%_4ZLlfwLol zcaM~`cSmhFK6`%;BC%}HexF6@>}a%mV#Q?lKI1Gd^{qSF z@yG0=G(-{?e)<*3bHCtL^<;oltA{PvVHsAMtdHjC6@v(5lCAaYdM~D}ibq-q1TL*nA_lr0?B5pi&U5eQp z#NWN3w4bJAsyC)!D-k&(;=2B`NZx;Bk^o#b@c1XfuUk~!cgf#s^}$WeD0HL4cO^4A z!l%!HbM%?2p7b3LMAYM~pu0(dzt=KRa#s)4tZ+-;gpG9msJX8Z!`6STqT>@Ze6DQc z;H%V|_efy}5AXQWjc=Pf?(=0?HR?{?Jn>QkrQxS+30v)X8Pz5#V);y(zl)yt76SFG zl#;7fqnmJZct(6|p$2y!{)v)@HKc!#9!kc*!|lk7)cgv?KiUb3?`+rU$IkgscXwU4 zCF93v^&*0g;;ijB(qh@G`5c}n`0aEy;dAPE?B0qT@0($i8oVyp%|PT?F|tKYn@!#7 zV3;#}tdKL@&cpBD{J>3aD+f{b=S+3YWJ1g!q!cPjCx?r+d_1+>@wDjITPwpgl7AuG ziEBW)Eq#*$q?TRW@<;o4_~5m}kC)Pt{T!0VzEhlqhzit@oQnR?k7UrVQmTsHX%=4E z4|KX}v6r~RurN&f*Jzo-&W~#)2{#bCq>t%)Z3x`*>8v33MDgOMVp^>$|xu z#y0(TrpN1^*5r1wO;$Od9yiMbljIfnZ3sN%mkZ`Fo*nT!i`bc^nvhNBvpVf+s>*Jc zqTt?TEQU=rC5T-`1v{>y&t`Sf`Igu6rBgVSPF$pdgi5CYidKlef6lUs)`xGbOv5v_ z3gJI~qaDgf#3Jce{i>}e(II@;*;%+KI0s|oQ$+_sH^H*8r*7q+K+Bb>p5uyZPGLi@ss_2C@QWzILYE-P+{K`Tf_E0P1+8czZeEEsQ&nXaYWx1^4pYg5e zcG{)mHa6YtuHl@8uB>B)XuT6%mTIUA@6n%>*{03tSwnWx&HQd6|C~*6+7KhHGyTY= zjDed>P&~$xz`mZG<%Ob+P!Wpgn+cwq>T(9?WnJYUAcV+3be8nY6z+5FQ#+rf0i>rJ zFntEJ7@aaQ6m>3%uEDbn>2Lp$g^r-8q=TWMRH(JmZ4gFHgV?{NS}H^h3}a>SUf+mP zD%ObvOxWqNn7%X~QxqI?Z^|6x&4<{mriWQ(*Zt1&Q3Bcc)1IoL-fE%zl|ZgOsPDg# ztuF030%*>-wp(E#gA3D*%=XY>#Tl|KhA!M#1QS}|lZFG+tH`(Lp|UargHu-(lc(Eh z$!N_w+?B-~g7`18z1MO+3Bdu)Xzl`CnIgl8xp#pA)@(#vC`6lhWH9)aJMoYwH?!$P z2wtH3VaQdYJPS|txKSsa^OSvY<$jrx`>E01{WeOFA;@mE>^RK5s+sf<3S5UT&Rf2X zo>MaA!@+&HgOEH-F4~!Lh5>LuChSXq&25mXM<>cVz9Y?mUenE1^-SNUba$c)q-}%S z$wG^qQC-PJ_rPA8F0bU5p)!WPqMKp44lpT7dDg>OsqF;z`L?XevMoJ!Jvjh}z=LGo zq>U*JF!&G4K@e^H)&p7?8&B{@n8fsDEJMj5D<859l?V4iq%HVp{KIofdR$tsnTOXG zL+G3dTM#?ISggG6EkyGbkuf0o)ndy8K9?p~++Xf|5-oQEj8Av$7l7a5>GG)iTEh)hi`&#u zgydcYBz}TUbm>9e3KW0*InH`=0hZd3v)WLA0;^IrRBarvzW2BM&fLF%PTfIUADSqS z0hNBl-9bspoI1+O9qes96MDG3rwm%ZJHNx8m&X|B>(jDNnnUnhHZ1Qz~S(0HAx_% zU8i|Rh{YD85Y@yZE+o}pMoyE?^Gd#zFR=cb<#>DF(onSdF7pS0h1b3QX|Js zk$q6NI?h;>*}sspXKn1XRrY?eTP@ekAOUr+(_mJdIms(bi_b_~&9DGw_Zg^{#nY^9 z5Sa$$j_Q7ak7p`|d@AC*bJ&3mLr=;qIw*Y#8ApC;9P8$0uVGAh|K zDyD4xH7G*xG#v|C4oslP##WTA1-zv6EYVRKJ z0*%I`)Bm7llAlU}LFE+0?4|VJ_}mq0$ho;Oqe)FsH>v0(t-ON67*=Bk@k3X>)>Z+Y zWkO}0A@?1@<=w?qX)yyv7U|nSG?%h;>AEagi!ImRWVQ6>C<^I*?2a7L;Jw)_N(?kh zIQBf={X!=rebH}^QS}|#D19-VL9x58#7>Z?Dm8$+1NmOAEJn<(s${E>oR@j@^4))s z)!!iSGRA;b#uqJA)M{q52cuwk@&cDd-KAb8q`S}U>YKTXngiGCR?BBI{PB+X4{Bz- zs*>THpt&;GKV^6ic0jUP2o>k^3n`<`2D4Rf4x?O?&~{XqCv9svrLw>#D};KNF@)-J z!BG6+A-_u&!;72QSp@>-4`qjCc#ep-^0Wp8QHtDIM`LOwXE_>-KjWu=0edkP&$cB@ z<-~(r@twQ)RpGABD%daQ)lm;`vz-YqXLPe?bE?|w(HlLyZ&Q&93)WT^(wZq{W$Zz^ z;28t;ympbk+l@s)NS)0}pDjj@pUJYaVVVW`aoP3t@6~ZCY(E1^Jqi|Ghu3-Q>}DHm zG?AV|WP}@q@Ph=U2GBCYw{$*ySQ)idDOXv-y(~suDuB@CCKq?rw!CYqLJ3hWEUiq> z@)>{=l56DRDb7p;lC* zUv<4C5YrDOqfPJb6Ez84VW5h0>|JLmUS&h#H#lO{lpSlrmZLW8iG9(HdRI;p&#okt zxpLAW5FL`_7ndsMyE#DyzF(M6mCwAaAof>jWNYPvKNytL>K~;gYa6#eWh^#UmDy!>h3wb_aP%deI;XzR$>~8H|F63*Il^shq;E2cA;V zlI5Q(Z2Y>}I6n+loqv$gd8nJ1Ym2!1dLPibma~Ei50;stE&%HI#p?L@Djqm$S@*yb zpe!i9%-}%b68*OnCiWmr+-6J2rW+jrBSG>wvjWgzeaLKlA3uR`VxsX?iQ?j89?F~A zVx%s8N%Lh>oL@q9UxVKC#Tm--<&@=K)c|=q~pJP{|-)K zF0~m7E^e!Jt~6>ln!Fs)8u7fT-FW(1SU*GExWnXe&Leiy?wqe04)nvKu`cAa=|HE> zlKaH{j%zOqKdxVwMH$RD-(kz{Qp=53x6k~zcpcl#xz4{=`c1~mL2)T`LmpvC&2R5Y8=dbs#?dIw*3t6o%?`-rt=Gm;~ zdOlRmoFUMJoYtY}BQG)7M^JD-is9PMvM08PLji!kyH8LcS+6Uw1U^5ODAkBHd{aIcK2_(oB23>`5r5n5QI@{8|3&BH&I3xj4}br8 zUKS31B9QSW6%ELElKdMlq^-Lw*1*XlK(&aF0@OoHjbfZjwku@!j=h{SiolBhczrNS(2qrH`Q|raz}Nen zaBGR+D?`6xp>5g^_%0oDMtWiHg|x?U&X=jUe{LDwra{Fa4n-qQO`$89vXWwFoKvlu zTsG<3U1K31rr$s#y$*}yJ2kHYd?)T&EkhdqH^kxe=kPH_6nrmmLg{aagVEUBpc70> zu326bbKT6jYJ|dRMYOlvgA@LSIH*oin9vP%xu9lpDJEH~Vmu|Vb0yQ?Q79`@LMCEL>uvLIxVC77I~x^ji|^q+s$z8XcD z0v(8-sT?T_>BuW2txzFj+TEA{#X;tI5UKmvg+2+h0ZPBbMv{0NkC5Jv# z??Ru^M*DU?lyouRY+maJ{c>TCo7`>F6mR1N%GM~Ux7cI}UPXyeYC=LmZJKzk3K}tl zS6RyQTzj2>de6GG`b~P7W@;XO%0uyv0bsVy%-rika9L!F?`U6Xw(eynztX?NI&YkX zxD<%-=W(dY^)gi-t+yJgZ`W6GNBj`I+B$EGU6Vw@C+&tqtJ9*s36o&-6Qd#JX7e{@pK@E#HyEVOvYoU zJaMkKHNRJC^0@bf+uZZFsGj8Cm~@;}40d_Rn7d@9tI zU98!%g!a9)`*Xd?nTy&di#hmH56>ZEX}AbJqg3Vr%3>%A9f_b&A#LX!hHA(L?mO4C zTzC5I9F-XQAT-p2=#v9$ z?&{xY{{-p2r;KO3Tuj2lPf~i>*tY9ht#}eSR4!bG@j&76In*xbnzXgj!aewVmU@2| z$B;9929of`MaFH5j?!L{v)rY0x%haDox%bpn9ANWo5||^*^c*ZEFvqs$@Exz?S(`? zi@_DVgEsamUTps8RP^&2=^3C^x;H&boeQ7#|4G|aMV1J_L;Z!{v=GmDv2F=Dj}%}X zm~|v)UF!{l|4K!qMANKV$R?3^J0)^v485)B?BO5g5czrsA67hHV53F2#Z^wO6n)1gt9 zqje~`D05OsES5U`NXs;<=^yap^;K!(A^v6LnE9Z#0U*c4=g8?Cccxl_>1M1HS8uvS zFQVg3#5Md~QH6p_(lcM^<3xzbYQ;*-PR30PA?S|6kc}^Q`NX5`gWnn!`%m(pG-6;} z$KMsJb&6Bz#I1D?tX3dX;b9WzO*96U+LXO#8+O^0eWx7}d@`2&;$lyT z0<2m@1-H8Y%)9P9uv_xyYwY)bzI{sg!$~qG|AzTJRPHuTZCTm;AtZ`fEqi+r`QEna zOY}5DK!cH!71!}8`G-&Jualft!AhVWHEkI{+;1gIlD6Yi3646j*{KiFamNQ;%;1g` zzLH5b4ZXsJeX8Hpb2lu%TVntr-+p`9t>Kkix36Sd3`<>($8uNGK8M--arwsfn{QQn z!FwqPk@hij@hs_wLO!X*MdA!6xbebo;dFVn*&GwvRxJnPvNXNsoBbrTWzEGtdtDrd z8k2UHx>QP27(eAW2eO-!jN)SV&@ct@C|Lqfun%|L8Ygj4oRX{m^&M1;1~1`YQfVMB zl9q@IN{!!ASqR{3cOTr6AQHeC3W$T_@a;nBw0(f%j^mn2tQZR@W(!DsRokNn=#$K? z^nm(Fs1arFUMp1nV3?m3wEo*>Ga5)i56}zn*tzAD0@>R+D8pI{VhunN0W@2HI?fZ@ zt0|7+zvV*z44UfjJt0~fsUUR_GRgJkI~>5n^7I$fga8pe?q!9fSnBdS~~W<5K~Dt zo-_b7_J$Y_StEMGaMf(Mxdi=tc2T0eEC_Q!h-C7S0i=D_`arb6_#p)#${0$Iu|)B7 zRD$|mB6fLSV$z)9Mm$En#_DgA0BCnJci%0Bnj*Z&SipAFQZ5z;_g|5&D}=$^kJ*cO+dgz|yr%=HokKD?m=eBfuD`oe zM-+TaQZ%=_nv1wG2*gCjY|_Ii&KZiRo1V|BRqi00CIf-g#4gZ2&q6rjD3BD74Vx;y z4VP43kv=$!y-AD};W00P&2REym7v}2Kct@k+paH}|LqZor*_U|g#`iP9)UkK0GOu( z2g9!2DAaik)t*ah&dXmAhn=1F<9x)3?Tm zf2|R~pDEaE(NhX(oI`R&G^{QgQ^Es0He>1B5;g)y{3(43j}0gXo)VGW89)afu7cD( zI2j(eAhwKRkK>sL-#n>aM8>N0Xe==L(?c`|KPf=^oYtc0B8{~c~|J@Pxuc4aFSGE zx?u7=Kf1gSvg3!exJe+_A4nxa?4x!D8Ue`z;gH_c-3XlFoN0qWt=3*>ODDF2hrL9z zM2hfG2iiGA0?4M{1W7WwHhXMSG+;?HJwLG1fdciEGqKv2EH=U5+O|&az#WpWg(rZ- z5oDMAto|tqM#Y1gL-tAupf6iebmA`+M}v?QKL@2tfeYDxn+148rdOlZ>yrc!B9?>L zPlNl^Lu~+9;EiKb1!k+VSY@9W%NFIE2F8S2S;pQEQ z_Tu&cu{jb+1cQq0n^R65-sz#-dptqW*nRKTd4le>6j`+QHTgt)5dbsi`8x;_q$mYQ zTmjsC!}CrD5hz!B9#YrhW{-!QqsndmsTVOMgP7n+?Kl6~LO=UH9KBQ}BMNUuSEEHj zWHPlB)q7aMIomwnk@UMoK?m6;V}m&c&uKj%vy_Snx0}(55&K;npIkT4ZreED;=bSf z9I+0Ok8UrKp~={dHgIg~3U^=L@hyT|60aU@6)BZ~jhq5f#nT7@y6$HlGC{(VnBvye z!FVd8O+kD`Q-4R`wYiz|oiSK?OdJa*UO$mP;lVt4(<0o`Y%<{m3-a=@vFpV^8y$#f zg!z8Rex@%4(%TvTYm0kripBhuvir^TIY9GMPe(kIz;$|dQ^rAHt;jVpYPcmu?k@Uo z?cH}!lW&{o@sLIcB~L;xLIMFoQ4?xJN)kfvMVc)X1rZe$8;T^M1*C+gfT96GK~ZTU zC@LUbse%HwP*e~LAS%+b@we~0?>l>T&Y80_XLe`K{>d=I@CV#;<$CVt`{_W7GK9tG z3e{4V)c24cYc%P8b@q42dq`E2A~Xn;3V`NnbtKjA<977EO0jdvz(@ZR(c~*tZIDfomgL9>!+Hi^QqJ_O z5QSrgi(f{9XNDK|DCvfCZ*>Jc6N(1iwKGH?-?!DXvc}~*s-0bj3Oo)IDIar0XZCNv zwH687!NUefY>n=_<)Zl-xAs>}a~MmUk+?w%;KW8=3C^?SBHHi5-S!mcJV^*^sXFN=$QuQ6yk-eQ&+og0cik(=%O{3Io;Vb@NQ>lgH< z4#?pceJ|gaz_>MO^#!8!?wPfh%nc)Ki;?)9BZ)C1oWhZmha;(PM$*?txEn^ZEJky7 zj-HPh%_|%&csP3L&FJN|QQn5JVvDhoonzNx#>xuEZaf^Tcr$i;ZLD&`t7?l^wL4$c z#rzLp@Lxq3Tz=~T9(Y`Rc2=vcss12fMttd_Neq2aDIR+D#9py`1$hMQvt6-saZ-~X*AkiOlT7vuNcv1104*% zqfY2|{RI+-uMC`~3_XxbV2eqpcPAbgc+v?^%Dj4g@xpl#vCN~Wy%XW$RD{a!PEir= zva8bO;ctbM`K$vq0=}jwshC}*1un{w7y@z`ybUq)0{%DjO|&_8NN4<|rhw&HsfT77 zWW(pdE9sc8tLTG4n8P00pDFUE28;K4&U>FjkB6g)D0G*oPA>PB*tleT4bXu9jvH9i zY&Nbu_mU6@ihVH*M?)>_e+Zb8dWWBbaj@`M)3hRh*Ya6(;OBTTcJDVpijjU3XV~?m z9W%cO3lsxw25vVgOeXK?oPOTb5SM&fc5}eW2J5Ma9(qw%{1K&oCS=?56|7R zf_|jSA^<)A495DQc=)TF@Qu3$z#uyx!Lp-AuAF%QXsgKl6ihW=9fwfoIA`ww4TA`^ z0$?H&7LibMRYWKvJeM^9pUED2R#tv1*;QI(Y5nZw6@y&R{I}f|vA>|m1x+Dgz%p_c zGs0S9xo*hKODCJ%CvDS}@9C%vdz3U8XvnHBX*&j9*B7@LA~wu<+IgRHEc7g+@U=uq zF)53<%6ytLQe=%@5H22ilWXM?RR{nh88<5YV#LPb^;FGvX&0U3xyAJ%ESF@LUopt2!c zGt8j%v5xW{vBvD7^cDxpiw3Q$s67V~iJ5iqwAnlN=z?hp`_h=;va~{w^V0RyTjDaL z{g+1SrEXRjN}W9#8kUwY;`J1BC4yjco#^O4otkidMLV82GAN>)a8UeU$#9fMoRqf0 z5oewnXYQ+7O|=;Xx3(S#r^uTY*zy$DtAftU2nHN@rs0Ppu`KZ4gn&9FN9PnY#BYlZ zEZwoG7v!x#o2k#NTS-z%`@jn=dI3$L^Ty@Er!wVzF$C_Tv-e@*EPj8O;nI3?ONrbb z`XGKoc-dw>^PHCVDTc8N&k{H1Mo$aW)OT?LiLB_>Zg>Pgc}9FsyL}E0t0irpJj>w) z8YbdByOWd!{vAB({s%5Nt96#GZDyj^jGBwwm<Kg3)U zQcpH_Z-Zg>;w)p>5gIBJvSGSc8gIH|j}yLxZNk^q&t8FN0f?luMt-`o-ArtU>Ne%thrhBVhw(bn_w4%~vD#^H@qBr(z)5&_vwQZkF zLKRX(X!){#V*&)pTSPt|XcM2=X}?=dD=aOEDLCbX*N+5!QIpRByo=sacG;(h-Pm?} zR$(z=Yk4ZY9<&z?#oRSekq;UVyAoBF=y|gKd|l$j-3qLPD&?h)iCfqZqi@FrI)Px% zJyW=ZN8f$?u#VdwHYLjM;!vR7JM$Ps^j^dWT-o-59}C0qq++v1)$^`aB`a*L=?<2< zlpr6S!h+^Ij_b?~Sg{?8JFG90Wa@JP7}7a?2sT$P#UNr*Z7@)=$at zO5lS9MfYM2c9hF1V;iCq_O`yUN~hmv;57FlXQ-o{KQr+7z|(>zH%X~Z^eeGeTIurw zkfK3zc-vJKgB~i`@07_K$(&M{^Cp$sC^1Z)q)*MUmFJsR zM;g6Ii`KUuW-5L922*G-^ctbSP3;X*y(nygOUn|zC+y}a3{A(Db zhF?&->vzq@Ig71lJ{yMY^bl|2#6Q`eCaG zkM0kI3mY?J>|ZHNJWF6WW3|;?>emOh_ZNgnag~P59*J;v*-OyaIJ zyGB;!d#kg^wty~vaToQOgDfVVZ16#CcgQ|X;Xm(Udx_!$A{zr0WF@0P%}p9&6X_o2 zSq}Ez>hkVSHs&1W#KyxEM?M7{5R7vUXAv86L=r;pf4G>?dLu!cO4F$xSBI^(;cvh1 zJfvE|-G#u{W4Al0#El0h+FRF$_FUxJ&@u>|J0S4BWNB_?p@pM}K~F=8z7!CD4vB{z zIH!A;j;VLgz6RZ)_~wHgjgfBThm{?2gdbzZS)BFMz`4`L3bN7Lk80}$#S@NWs!MEE$+Av4}H`7*`}KkhlQ$hC0m?_%~KMS7C%EgP)BI* zQ8`6Kk#D1Axn&w=e(4J`n4rWk2du&`8=6cF5G(x^yrk;=SOFv9XHarNf0}hd9SgvQ zlKT4+z6J(YT4bSPx_4}GeOLqD5Att^8}l7e1&=c2^9l`eyneXx5!pgdYU1_2CYV9> z_t+8Uv{{}{@=3<8&Z(H)bz8H>Y&K*cz1Vs@OS{Kib^_GnbZ*iR2$s}h)N7=@d8-w; zI{#D^hvJ~}%>_cAH*$LL$@V5$=4sw6a zaM88fj3+4@oa*#ls8HYhKV8>X_aNI&g^1p}{YKbz9gMgpyjeexX?{9AR=> z->9ug#t2&M-dz8uS4h3T${@K4Jw=Zn_p?FO6PjdTRc&aeDeW2pxL0O|Cf9C+w zUkVcw|0xb2X3S}QAdABYELj1izl_rt>}VR$II|kpZ52vvZJ{&{Gqd8MGtArsk-eRn z1hCBuuBPYbpnkQuRl{~GVVJT{^;r`;o=RDjPNW!yg#{lmAK`Nm>IyO}3ROY|ET%qn zqF;jUy+}(7_Lv3|o!=5lsUr%DEmpoJ^l>EY$6SWQZp+_bl=-Ukd+R*`CDwph_f#om zpR10xoo%DN7uQ$mmpcTSFqa`|ekqxoE#UE*;tXLrVb;`C;>W9y`36NBBR)Tr8mGWx zI%ozJU$^l*6*jE6n}Xb@1|ApjEEkl_KQ{(%6MXMOT)dUt<}A(kNGxx>O}Eyn0ZNJ~ zrSb7kDXvkAY$cT~Ko;wvt+_YFMTlO6a zAx~vUB@1HoS5ii=tYh2_g6+~vQylN#N_{#Dvy#CNQ_e=VezG%sQLScuL8}%a8Gyhi zQ`BAYzILK`nWt9c$j=+{)NzWTRIA>mP-}fqAP-F70;EDxvy#_Ilhnon$`^$@bTck3-S`yCUz@M)f<+GETmJ^@G5L7%&?ac2e7!OH(A6 z9v8#G3OBwvC(a7btrCM2N}YR9!^g6#@Fpt@v$>|t0&X$iD(3pZ2m5cLhT4t%Byy5J zz$E9#zqCq!7zu?OyR@oyx56WGM68*{Rof+h=;2P2PWKiOQ8J@7R9M-2GAS8|TT|=L zid|cdaOsbeI3|j2EufBZqpZ!&__V`jtG_+=m+q%dM)k+3w(`%S=d+gAJ1EuAewZ18 zg|O#?G30XwC>)Ct2|(L&;c=3+8HkfW9Po?<Y#->;H3REfEt(Pvf zB$bV`BxC?25#T}ZV(q0w`?*0qAyU5M-H06?aDvp7oH_!e@KS@V*h&$(>HY+vgiUcM z(bMmHbyZ!>0HR3dNqQ=<5bP*U?2r@A3$T|N)5;d7%&HX^ixIHJFh?LL^V{!yNp*m& z3XI2q{aI+sWUhQAFPL;OS%g__BnN!L%J&b5&`ba*fMv#6zdy080H)?lNM9w#FRfFx z)S+Uy3o2ckWhdfPv4Hm4*3Q9}a_8@h9?;?@n~jkkDVQBI2gMq)5+3>YMz7O_n#u;{^2h0=?T$&BdL@#el+!|FR-!pByb$NiBUR$E=GUK>?c`&cH_L^zWjO*#; z=RB#qWp*x~JTLxLk-6d9^U3GI@^EGP-HK3`*??EeFY0^m-agSYyK80nWs_80Ws=M1 zAmx>jPS?8X3q7CrZe1DePp_-J?(!vESdkg&t*g7&^X1^_l~;VJd-WYIUyoc|c|GlV z@BYi4ug4#(yqQbC*Z9%pTlA}yw@bbE9{uY1c6Mdu9SBw5Bs{IbeNxYf7F_s%75 zT^$E9>RYv4=To+?PAK%%x109Pr=MP(#G)EH?OYeKF0M`y1a1x8zP$_QAFRG7W;8qt zb^TuOYV`xTuc7xu@Au0qs~;(-`~69-i^a-o)Anxn2QTz4Ufa4h4%eU67uPudaA+V{Kn^!j%m z>fyAV+w$PW^~Ex`ho5}=mR~$r|52Io@bjJwD8A*ap+i%v74dNAi$id+3;k=e`b(GE2vrQ(A8JZHd%o#(Ze{Fu5wV{?V;@?(8;XT-M+N)sMehqy{CN^M?j1W=6WEvM{Fa}5yb=>X$K60myo&=n5I5U>P^uHfT((Dl z&{n!)jD~HI$B-Ccw73!(UDv<6?={g8KOnPLBuN9ERy>|ahq~sfxo7RKr|+D$Crp9oWVOYV6TP08wfj zhu(tTmjkBd19JI*dvmIQBJooA{Q8va2{9SRQ;MWAfarg8be=46$B_Cl~A84%%rKkygZBrks5`M(bQwe}fr zy5nb!9{qjb|MYs4J6M}hdOW42t)^_?s(rRFP%fiGiq0)?{M*34|8n==0_9&r@O2e` z{I5;Wy;b>Re`R-tf$~2C{~!PB5KT$ozO+IA2puUL_zyuV{^5Tubh(_S_zV57qwfyv zo<@}X=xcgxds%4gd>PK5;wyyN-G{>4D6m?gvEy=d*hD>I9^^RPWXNzUPW2I>a6 zM4Z~CFeR!cD4Xvyytt}?Al`g^oy$5o1DZ%T8OL%ZAV2ycG@J^wM8bOAnud_3H}^}J zLz6CKnERAxiNj|I+$4m^?>=!QvsOs1_75x%MYj2!A*CO&Wk4bzK<7HuQi0`_CAV`f zqp*3Dl8XxI)hV;tRQnrsF!1c6C>U``W|%@5!OqaNCBn;nqoFVD?Cggo5-6K`qu5Bq zmj#qUE76bUsy>ZwFRgSGMA@F@)XND^9ix-aRe;R)QVIfkmrH<**Lh)gx0(^u&U1eN zy0TL7T2<@cS(#cF~*-g?@E^5ccFjUDGr_J28jFI4vJHUCa`|p{! z>=G!QmpbDnD7{)@{CelFV>x7EGetUwH`yL%n*Lg$q?uJanPb|KT2n};%h0mCzcvI% ztM=QvpGSnm7fO7&0Ek?A`9|$n z-%$tYdDN`-5+h;=OuD`LZ`^kE&-1H~k)CZDl7WU=X(zMgwCkR1hEPKAf|Kg+P z&yqAFcz23g9q4zIjGfxPUwy5^hy%9g(~aN`Fr#HV?jdJQE0NS;kd}csbWubROQq>W z{eTUw2FJn&p1`n6C?8^57}_~aLaGTVzQ;xibGWV8p8X(pbzF)uuoLMO94A)KfoU)$ zH3VGdEly6L)H}UyYy^>%!>0hY7>?YTL{14M$ZcS3d@p^@L=k@wZsMmR$%!N<8X*nH zz!-%cT=9J$$x^zKCB6fIDnH}H%&;`nvhWY-U}wf5i4xirxkIUv2e$xWUdUj0N@0jo zDy$N$woaB<&KGlRHc6U<+WgN zfuN0Oa@UzQct;}slV)q0Ue7+qK^?XWpDiaG*TsDLYgv4zD4j64ED^-R*e`6dWW1vu zPXsO|#>2yh_Jw;gAn2$Tx$;Vwu5h)c17%{D^f(wAmR->fN+2_y+4e4>HkmMv7&)|A zRV?3)KZ8-vRG?d%m}W+o@~;HEc`D&Emtj6QG zVvVGwj_{*L5bhC3xiGGj5D46nq<{!tk|9<^bBvf|;x~cr4}W5Ha$k#{5)@Hv!!eZd z?5&+1J}!0mZS2FzI3S8GSI&d!Li^$3I)S)I;qBC>CL3ZZr5+K;xBV~+LdQpGS{JZg z`!#{%2mFksr>O{n$b`3F5E!m3luj{TgE)zCrs`Ltxk5#MVMPwg?i zc4R5UosFUMo;jTtk&>GM$jQG2J*e^R1;fww-e~hJjZCa5fnNA%x$?V@DJ9I+xJ=WO zm-{)cwZ;E4@c;K1`1S1T>C+AgP$!j_-#0PVtag1nC7k`gS7|?Fc;0dAeaQ&AFb&lT zdwfd9nQz&m{m5=k<;0|RGp0-4Uh&qhv;&Md-N*9wb4i;qM)Uq37MY>~i;AE#ti{We z6O1d;?(zpeT3=aFgNXsv(rijpJ43MK9plOZjcC6A)o~r;#8oI=#9U>V@1H~2ZG$|! zC*QSzTdR@*Eul(-G35+gU6Xe36M0y@6pH02zy_m5@QuRPmVXGZzKiYQGUq(}y{b;% z@Wt!~A?Y4A@hq?fMG`22*HM9?YG=M`%X%W4zQFHZmg zOt4uC*xeS`!!huLq45l@i{oHVPp~l`Qc~?3-<;qhsFIqSL%qXF2YW$2ONT(I+YCjR zeW4h#XOa&KqEoW=6jmNCFpWpRtMPip>;azDZ;U~J^w7vwtbWkA20a9D{E+4~?w=pMyp5N7EYDr*jMD=aNCQ1l8#qJCUEv{Jm&9xWb} z8XXw>+jDm$3u1}M^cy(myiHN}H?ZMIo4o3XxD)_kECLqHL?a!*m7nR2+XEI+h8HK} z29nA;Wylm0AA+n|6W#<1`7XP+hNbnrRH3g@fVlHxATr4nS zZ}a(WBz$d{s}vtJR-JNEglsE@p0z)0i9Mex2mF(MfferE|IJhTkN57y!k40jE7H4F z8XXy`)@&7>YJ69=mRIqP@@mbe7d8dAT+*r8+kTOJ^zTlP?4bBR7+zt${aR&^Ne)r9|Xf}VH@9A}oVYYteQ-1Udy{j+!XGj9Il1PRCRcg5X*7+&Gt-7Z4oFDK}~ z3w6mheQLw_jrmuG*8@^L*S8V{UUH_>Jq22!Mj)jwV_eq?CH|y81I_FjWdz7QQRCYF3WY zz6N&E&ALSHhtijLJE{=`<*^h&>O!Aw>FOk{2&~f|P`Guixy#BZuk_1ajcGsejd#xk z^%vgSP>`wfE#0I=d=hEY8DGA76n?Y`LAQQ<3cpE zAwwM$?P1hP8BCv!%Ci`94=rnSP1Anlf%1k(dG1fsdSpCQSvGQN41^!!ynQBQcz2IJ zxpeqR@}VooKHfT!WBv6H!%LL=oxx}Rzh`*=Ki4mSC|RuDe1@z0kql|TUK`xdW$^+8 zgI0y8Wk+^d+~?Uay;%ruq?pwWj96)Fk?FceTHPg2Y}L@D-uBUso$$l5qLp&Kt__a6 zmfbPnc^2_5&Dm#}D6xMj>1Gt~>MfqZ1#-n-G;#QGSng{`i#*;smu=J(pwD3&D_?L> z9)Ay$T26Yk={aOWEB3OVqAv-3BHQG!Bz}L?J`ndsVm|ztp&mbC2Lj3lr#9~?0-tIw z%!yPKwuHfyf;gWur|QF`T9E=`0F_IWzZtk!K@izg=+pKr-V42J#RvVGfPejj@^1q+cvsP*r+Y9F%|8-qbEIWUP&Z*MaL3e-P zE&A8GoT*Ckn511x+&)X-6Hc z1efZGLXvoY;icfRX%BTyI^1>lACmZ&C)!dbwJIK;8)sM(@VmD=SjeTHV5x;^Ad`i8 z?lZzi)}r@4z;A_0k+z)lREr7jO?E)0g5F;lK42jMZ!Rz1;scK(87~*v2qNi zyF_fU(jh2J*o%{fUZ&C{L&JS8}k{lDks=aOq|;ak+IRY;j#_BSj&Xo~US@?>8otEe-hn zAB#8(OV{&LrrFmoCksj97~`CB@r|_-0n;Tyl6WVg^fUYErt+zB2=(%{8{zxS;)xv1 z$^}8uhif-qgx?G@?j%x0U}Ua#$vwDU#I7t^y-}+~DIu30IztS|t~C(4mpe6`gj~A8 z-QfS>(*ILj`hWd7?HVbTBYc#g8wgb{ZpD&^0yU2kCatqUq@=B%1JO=L=s3?r8z*S; z&VsyFLi&`U05Ktz9M;BlAJ7yqgXAq1Y&6qJc(VCb`_JlJ%~gnWA?T_SHLHC~H0fL} z^Qy|1kxb1gQu>)yj@l0e`wR&TH(`pS{4TH4^7w?>)vzgBS0I!5PxGQV*hWMWgb|w6 zM?r!$uX&V1X>1`9xx*evE{)IQ>dQ3*87Tqywe4xk**0Mu4@sD(z8Wjw53KaUNQNZAHXEXyNa%S*W&E;Q#V11!!mCc}SIok&XorfZ``Cc_Gn$ p(p}vq9UjM762(p?C;sya+82CW_@#*OXFx#$*gqY%|Na|W{{@fkOsD_= literal 0 HcmV?d00001 From 79c7e9c267c38b08f7db1ab336136113fbb21090 Mon Sep 17 00:00:00 2001 From: schwar3kat <61094841+schwar3kat@users.noreply.github.com> Date: Sun, 19 May 2024 14:19:06 +1200 Subject: [PATCH 48/48] Update Home.md Fix typo's --- share/doc/armbian-configng/Home.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/share/doc/armbian-configng/Home.md b/share/doc/armbian-configng/Home.md index 413391cff..5707df02e 100644 --- a/share/doc/armbian-configng/Home.md +++ b/share/doc/armbian-configng/Home.md @@ -20,9 +20,9 @@ Fri Apr 12 01:33:08 AM MST 2024. *** - ## **System** - - **S01** - Description: Enable Armbina kernal upgrades + - **S01** - Description: Enable Armbian kernal upgrades - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s01) - - **S02** - Description: Disable Armbina kernal upgrades + - **S02** - Description: Disable Armbian kernal upgrades - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s02) - **S03** - Description: Edit the boot enviroment (WIP) - Status: [WIP](https://github.com/armbian/configng/wiki/Menu#s03) @@ -50,9 +50,9 @@ Fri Apr 12 01:33:08 AM MST 2024. - ## **Localisation** - - **L00** - Description: Change Globla timezone (WIP) + - **L00** - Description: Change Global timezone (WIP) - Status: [review](https://github.com/armbian/configng/wiki/Menu#l00) - - **L01** - Description: Change Locales reconfigure the language and charitorset + - **L01** - Description: Change Locales reconfigure the language and character set - Status: [review](https://github.com/armbian/configng/wiki/Menu#l01) - **L02** - Description: Change Keyboard layout - Status: [review](https://github.com/armbian/configng/wiki/Menu#l02) @@ -68,7 +68,7 @@ Fri Apr 12 01:33:08 AM MST 2024. - ## **Help** - - **H00** - Description: About This systme. (WIP) + - **H00** - Description: About This system. (WIP) - Status: [review](https://github.com/armbian/configng/wiki/Menu#h00) - **H02** - Description: List of Config function(WIP) - Status: [review](https://github.com/armbian/configng/wiki/Menu#h02)