From 8d95c4b81da75a8bfb66eaf4d464dac5781d2c10 Mon Sep 17 00:00:00 2001 From: Devon Bagley Date: Wed, 27 Oct 2021 17:39:00 -0700 Subject: [PATCH 1/7] ignore dist directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist From 34c81328a6b8160f9c092a7b08105682ef1ce3ec Mon Sep 17 00:00:00 2001 From: Devon Bagley Date: Wed, 27 Oct 2021 17:40:35 -0700 Subject: [PATCH 2/7] build package for more distros --- .fpm | 18 ++++++++++++++++++ Makefile | 14 ++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 .fpm create mode 100644 Makefile diff --git a/.fpm b/.fpm new file mode 100644 index 0000000..332e704 --- /dev/null +++ b/.fpm @@ -0,0 +1,18 @@ +-s dir +-f +--name rebash +--license WTFPL +--version 0.0.8 +--architecture all +--depends bash +--depends sed +--depends grep +--provides rebash +--description 'bash/shell library/framework' +--url 'https://github.com/jandob/rebash' +--maintainer 'Janosch Dobler ' +--after-install 'after-install.sh' +--after-remove 'after-remove.sh' + +rebash.sh=/usr/bin/rebash +src/=/usr/lib/rebash/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..49c9048 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +ifndef PREFIX + override PREFIX=/usr/local +endif + +VERSION=0.0.8 + +.PHONY: package + +package: + mkdir -p ./dist + # fpm -t pacman -p dist/rebash-$(VERSION)-any.pkg bsdtar required? + fpm -t deb -p dist/rebash-$(VERSION)-any.deb + fpm -t rpm -p dist/rebash-$(VERSION)-any.rpm + fpm -t apk -p dist/rebash-$(VERSION)-any.apk From 8d9f51d2027f666c56017c9c9161d986adc828d5 Mon Sep 17 00:00:00 2001 From: Devon Bagley Date: Wed, 27 Oct 2021 17:41:29 -0700 Subject: [PATCH 3/7] add bpkg package manager support --- Makefile | 8 +++++++- package.json | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 package.json diff --git a/Makefile b/Makefile index 49c9048..36fde0f 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,13 @@ endif VERSION=0.0.8 -.PHONY: package +.PHONY: install package + +install: + install -b rebash.sh $(PREFIX)/bin/rebash + mkdir -p $(PREFIX)/lib/rebash + sudo cp -f src/* $(PREFIX)/lib/rebash/ + bash after-install.sh package: mkdir -p ./dist diff --git a/package.json b/package.json new file mode 100644 index 0000000..31f0455 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "rebash", + "version": "0.0.8", + "description": "bash/shell library/framework", + "install": "make install", + "scripts": [ "rebash.sh" ], + "files": [ + "src/arguments.sh", + "src/array.sh", + "src/btrfs.sh", + "src/change_root.sh", + "src/core.sh", + "src/dictionary.sh", + "src/doc_test.sh", + "src/documentation.sh", + "src/exceptions.sh", + "src/logging.sh", + "src/time.sh", + "src/ui.sh", + "src/utils.sh" + ] +} From 88d2fd790838e6a3b86b4a5c09bc424b17e80a2d Mon Sep 17 00:00:00 2001 From: Devon Bagley Date: Wed, 27 Oct 2021 17:43:40 -0700 Subject: [PATCH 4/7] fix testing and pacman build to work with changes --- .travis.yml | 2 +- Makefile | 5 +- PKGBUILD | 4 +- rebash.sh | 22 ++ src/arguments.sh | 241 ++++++++++++++ src/array.sh | 166 ++++++++++ src/btrfs.sh | 357 ++++++++++++++++++++ src/change_root.sh | 146 +++++++++ src/core.sh | 406 +++++++++++++++++++++++ src/dictionary.sh | 143 ++++++++ src/doc_test.sh | 707 ++++++++++++++++++++++++++++++++++++++++ src/documentation.sh | 121 +++++++ src/exceptions.sh | 329 +++++++++++++++++++ src/logging.sh | 481 +++++++++++++++++++++++++++ src/time.sh | 16 + src/ui.sh | 220 +++++++++++++ src/utils.sh | 207 ++++++++++++ test/mockup_module-b.sh | 2 +- test/mockup_module_a.sh | 2 +- test/mockup_module_c.sh | 2 +- 20 files changed, 3572 insertions(+), 7 deletions(-) create mode 100755 rebash.sh create mode 100644 src/arguments.sh create mode 100644 src/array.sh create mode 100644 src/btrfs.sh create mode 100644 src/change_root.sh create mode 100644 src/core.sh create mode 100644 src/dictionary.sh create mode 100755 src/doc_test.sh create mode 100755 src/documentation.sh create mode 100644 src/exceptions.sh create mode 100644 src/logging.sh create mode 100644 src/time.sh create mode 100644 src/ui.sh create mode 100644 src/utils.sh diff --git a/.travis.yml b/.travis.yml index 609427b..811be75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,4 @@ sudo: required dist: trusty language: bash script: - - bash doc_test.sh + - bash rebash.sh diff --git a/Makefile b/Makefile index 36fde0f..0ed5af1 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,10 @@ endif VERSION=0.0.8 -.PHONY: install package +.PHONY: test install package + +test: + bash rebash.sh --side-by-side --no-check-undocumented -v install: install -b rebash.sh $(PREFIX)/bin/rebash diff --git a/PKGBUILD b/PKGBUILD index 0f2e286..000335e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -16,6 +16,6 @@ package() { mkdir -p "${pkgdir}/usr/bin" rm -r "${srcdir}/rebash/images" cp -r "${srcdir}/rebash/" "${pkgdir}/usr/lib/" - ln -sT /usr/lib/rebash/doc_test.sh "${pkgdir}/usr/bin/rebash-doc-test" - ln -sT /usr/lib/rebash/documentation.sh "${pkgdir}/usr/bin/rebash-documentation" + ln -sT /usr/lib/rebash/src/doc_test.sh "${pkgdir}/usr/bin/rebash-doc-test" + ln -sT /usr/lib/rebash/src/documentation.sh "${pkgdir}/usr/bin/rebash-documentation" } diff --git a/rebash.sh b/rebash.sh new file mode 100755 index 0000000..bd3caf5 --- /dev/null +++ b/rebash.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +VERSION=0.0.8 + +if [ -f $HOME/.rebash ]; then + source $HOME/.rebash +else + if [ -f /etc/.rebash ]; then + source /etc/.rebash + fi +fi + +if [ -z "$REBASH_HOME" ]; then + export REBASH_HOME=`dirname ${BASH_SOURCE[0]}`/src +fi + +if [[ ${BASH_SOURCE[0]} != $0 ]]; then + source $REBASH_HOME/core.sh +else + $REBASH_HOME/doc_test.sh "${@}" + exit $? +fi diff --git a/src/arguments.sh b/src/arguments.sh new file mode 100644 index 0000000..9172f42 --- /dev/null +++ b/src/arguments.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source $(dirname ${BASH_SOURCE[0]})/core.sh +core.import array +# shellcheck disable=SC2034,SC2016 +arguments__doc__=' + The arguments module provides an argument parser that can be used in + functions and scripts. + + Different functions are provided in order to parse an arguments array. + + #### Example + >>> _() { + >>> local value + >>> arguments.set "$@" + >>> arguments.get_parameter param1 value + >>> echo "param1: $value" + >>> arguments.get_keyword keyword2 value + >>> echo "keyword2: $value" + >>> arguments.get_flag --flag4 value + >>> echo "--flag4: $value" + >>> # NOTE: Get the positionals last + >>> arguments.get_positional 1 value + >>> echo 1: "$value" + >>> # Alternative way to get positionals: Set the arguments array to + >>> # to all unparsed arguments. + >>> arguments.apply_new_arguments + >>> echo 1: "$1" + >>> } + >>> _ param1 value1 keyword2=value2 positional3 --flag4 + param1: value1 + keyword2: value2 + --flag4: true + 1: positional3 + 1: positional3 + +' +arguments_new_arguments=() +arguments_set() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + ``` + arguments.set argument1 argument2 ... + ``` + + Set the array the arguments-module is working on. After getting the desired + arguments, the new argument array can be accessed via + `arguments_new_arguments`. This new array contains all remaining arguments. + + ' + arguments_new_arguments=("$@") + +} +arguments_get_flag() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + ``` + arguments.get_flag flag [flag_aliases...] variable_name + ``` + + Sets `variable_name` to true if flag (or on of its aliases) is contained in + the argument array (see `arguments.set`) + + #### Example + ``` + arguments.get_flag verbose --verbose -v verbose_is_set + ``` + + #### Tests + >>> arguments.set other_param1 --foo other_param2 + >>> local foo bar + >>> arguments.get_flag --foo -f foo + >>> echo $foo + >>> arguments.get_flag --bar bar + >>> echo $bar + >>> echo "${arguments_new_arguments[@]}" + true + false + other_param1 other_param2 + + >>> arguments.set -f + >>> local foo + >>> arguments.get_flag --foo -f foo + >>> echo $foo + true + + ' + local variable match argument flag + local flag_aliases=($(array.slice :-1 "$@")) + variable="$(array.slice -1 "$@")" + local new_arguments=() + eval "${variable}=false" + for argument in "${arguments_new_arguments[@]:-}"; do + match=false + for flag in "${flag_aliases[@]}"; do + if [[ "$argument" == "$flag" ]]; then + match=true + eval "${variable}=true" + fi + done + $match || new_arguments+=( "$argument" ) + done + arguments_new_arguments=( "${new_arguments[@]:+${new_arguments[@]}}" ) +} +arguments_get_keyword() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + ``` + arguments.get_keyword keyword variable_name + ``` + + Sets `variable_name` to the "value" of `keyword` the argument array (see + `arguments.set`) contains "keyword=value". + + #### Example + ``` + arguments.get_keyword log loglevel + ``` + #### Tests + >>> local foo + >>> arguments.set other_param1 foo=bar baz=baz other_param2 + >>> arguments.get_keyword foo foo + >>> echo $foo + >>> echo "${arguments_new_arguments[@]}" + bar + other_param1 baz=baz other_param2 + + >>> local foo + >>> arguments.set other_param1 foo=bar baz=baz other_param2 + >>> arguments.get_keyword foo + >>> echo $foo + >>> arguments.get_keyword baz foo + >>> echo $foo + bar + baz + ' + local keyword="$1" + local variable="$1" + [[ "${2:-}" != "" ]] && variable="$2" + # NOTE: use unique variable name "value_csh94wwn25" here as this prevents + # evaling something like "value=$value" + local argument key value_csh94wwn25 + local new_arguments=() + for argument in "${arguments_new_arguments[@]:-}"; do + if [[ "$argument" == *=* ]]; then + IFS="=" read -r key value_csh94wwn25 <<<"$argument" + if [[ "$key" == "$keyword" ]]; then + eval "${variable}=$value_csh94wwn25" + else + new_arguments+=( "$argument" ) + fi + else + new_arguments+=( "$argument" ) + fi + done + arguments_new_arguments=( "${new_arguments[@]:+${new_arguments[@]}}" ) +} +arguments_get_parameter() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + ``` + arguments.get_parameter parameter [parameter_aliases...] variable_name + ``` + + Sets `variable_name` to the field following `parameter` (or one of the + `parameter_aliases`) from the argument array (see `arguments.set`). + + #### Example + ``` + arguments.get_parameter --log-level -l loglevel + ``` + + #### Tests + >>> local foo + >>> arguments.set other_param1 --foo bar other_param2 + >>> arguments.get_parameter --foo -f foo + >>> echo $foo + >>> echo "${arguments_new_arguments[@]}" + bar + other_param1 other_param2 + ' + local parameter_aliases parameter variable argument index match + parameter_aliases=($(array.slice :-1 "$@")) + variable="$(array.slice -1 "$@")" + match=false + local new_arguments=() + for index in "${!arguments_new_arguments[@]}"; do + argument="${arguments_new_arguments[$index]}" + $match && match=false && continue + match=false + for parameter in "${parameter_aliases[@]}"; do + if [[ "$argument" == "$parameter" ]]; then + eval "${variable}=${arguments_new_arguments[((index+1))]}" + match=true + break + fi + done + $match || new_arguments+=( "$argument" ) + done + arguments_new_arguments=( "${new_arguments[@]:+${new_arguments[@]}}" ) +} +arguments_get_positional() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + ``` + arguments.get_positional index variable_name + ``` + + Get the positional parameter at `index`. Use after extracting parameters, + keywords and flags. + + >>> arguments.set parameter foo --flag pos1 pos2 --keyword=foo + >>> arguments.get_flag --flag _ + >>> arguments.get_parameter parameter _ + >>> arguments.get_keyword --keyword _ + >>> local positional1 positional2 + >>> arguments.get_positional 1 positional1 + >>> arguments.get_positional 2 positional2 + >>> echo "$positional1 $positional2" + pos1 pos2 + ' + local index="$1" + (( index-- )) # $0 is not available here + local variable="$2" + eval "${variable}=${arguments_new_arguments[index]}" +} +arguments_apply_new_arguments() { + local __doc__=' + Call this function after you are finished with argument parsing. The + arguments array ($@) will then contain all unparsed arguments that are + left. + ' + # implemented as alias + true +} +alias arguments.apply_new_arguments='set -- "${arguments_new_arguments[@]}"' +alias arguments.set="arguments_set" +alias arguments.get_flag="arguments_get_flag" +alias arguments.get_keyword="arguments_get_keyword" +alias arguments.get_parameter="arguments_get_parameter" +alias arguments.get_positional="arguments_get_positional" diff --git a/src/array.sh b/src/array.sh new file mode 100644 index 0000000..b7e1dc9 --- /dev/null +++ b/src/array.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source $(dirname ${BASH_SOURCE[0]})/core.sh +# shellcheck disable=SC2034 +array_get_index() { + # shellcheck disable=SC2016 + local __doc__=' + Get index of value in an array + + >>> local a=(one two three) + >>> array_get_index one "${a[@]}" + 0 + >>> local a=(one two three) + >>> array_get_index two "${a[@]}" + 1 + >>> array_get_index bar foo bar baz + 1 + ' + local value="$1" + shift + local array=("$@") + local -i index=-1 + local i + for i in "${!array[@]}"; do + if [[ "${array[$i]}" == "${value}" ]]; then + local index="${i}" + fi + done + echo "$index" + if (( index == -1 )); then + return 1 + fi +} +array_filter() { + # shellcheck disable=SC2016,SC2034 + local __doc__=' + Filters values from given array by given regular expression. + + >>> local a=(one two three wolf) + >>> local b=( $(array.filter ".*wo.*" "${a[@]}") ) + >>> echo ${b[*]} + two wolf + ' + local pattern="$1" + shift + local array=( $@ ) + local element + for element in "${array[@]}"; do + echo "$element" + done | grep --extended-regexp "$pattern" +} +array_slice() { + # shellcheck disable=SC2016,SC2034 + local __doc__=' + Returns a slice of an array (similar to Python). + + From the Python documentation: + One way to remember how slices work is to think of the indices as pointing + between elements, with the left edge of the first character numbered 0. + Then the right edge of the last element of an array of length n has + index n, for example: + ``` + +---+---+---+---+---+---+ + | 0 | 1 | 2 | 3 | 4 | 5 | + +---+---+---+---+---+---+ + 0 1 2 3 4 5 6 + -6 -5 -4 -3 -2 -1 + ``` + + >>> local a=(0 1 2 3 4 5) + >>> echo $(array.slice 1:-2 "${a[@]}") + 1 2 3 + >>> local a=(0 1 2 3 4 5) + >>> echo $(array.slice 0:1 "${a[@]}") + 0 + >>> local a=(0 1 2 3 4 5) + >>> [ -z "$(array.slice 1:1 "${a[@]}")" ] && echo empty + empty + >>> local a=(0 1 2 3 4 5) + >>> [ -z "$(array.slice 2:1 "${a[@]}")" ] && echo empty + empty + >>> local a=(0 1 2 3 4 5) + >>> [ -z "$(array.slice -2:-3 "${a[@]}")" ] && echo empty + empty + >>> local a=(0 1 2 3 4 5) + >>> [ -z "$(array.slice -2:-2 "${a[@]}")" ] && echo empty + empty + + Slice indices have useful defaults; an omitted first index defaults to + zero, an omitted second index defaults to the size of the string being + sliced. + >>> local a=(0 1 2 3 4 5) + >>> # from the beginning to position 2 (excluded) + >>> echo $(array.slice 0:2 "${a[@]}") + >>> echo $(array.slice :2 "${a[@]}") + 0 1 + 0 1 + + >>> local a=(0 1 2 3 4 5) + >>> # from position 3 (included) to the end + >>> echo $(array.slice 3:"${#a[@]}" "${a[@]}") + >>> echo $(array.slice 3: "${a[@]}") + 3 4 5 + 3 4 5 + + >>> local a=(0 1 2 3 4 5) + >>> # from the second-last (included) to the end + >>> echo $(array.slice -2:"${#a[@]}" "${a[@]}") + >>> echo $(array.slice -2: "${a[@]}") + 4 5 + 4 5 + + >>> local a=(0 1 2 3 4 5) + >>> echo $(array.slice -4:-2 "${a[@]}") + 2 3 + + If no range is given, it works like normal array indices. + >>> local a=(0 1 2 3 4 5) + >>> echo $(array.slice -1 "${a[@]}") + 5 + >>> local a=(0 1 2 3 4 5) + >>> echo $(array.slice -2 "${a[@]}") + 4 + >>> local a=(0 1 2 3 4 5) + >>> echo $(array.slice 0 "${a[@]}") + 0 + >>> local a=(0 1 2 3 4 5) + >>> echo $(array.slice 1 "${a[@]}") + 1 + >>> local a=(0 1 2 3 4 5) + >>> array.slice 6 "${a[@]}"; echo $? + 1 + >>> local a=(0 1 2 3 4 5) + >>> array.slice -7 "${a[@]}"; echo $? + 1 + ' + local start end array_length length + if [[ "$1" == *:* ]]; then + IFS=":"; read -r start end <<<"$1" + shift + array_length="$#" + # defaults + [ -z "$end" ] && end=$array_length + [ -z "$start" ] && start=0 + (( start < 0 )) && let "start=(( array_length + start ))" + (( end < 0 )) && let "end=(( array_length + end ))" + else + start="$1" + shift + array_length="$#" + (( start < 0 )) && let "start=(( array_length + start ))" + let "end=(( start + 1 ))" + fi + let "length=(( end - start ))" + (( start < 0 )) && return 1 + # check bounds + (( length < 0 )) && return 1 + (( start < 0 )) && return 1 + (( start >= array_length )) && return 1 + # parameters start with $1, so add 1 to $start + let "start=(( start + 1 ))" + echo "${@: $start:$length}" +} +alias array.slice="array_slice" +alias array.get_index="array_get_index" +alias array.filter="array_filter" diff --git a/src/btrfs.sh b/src/btrfs.sh new file mode 100644 index 0000000..afefccb --- /dev/null +++ b/src/btrfs.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source $(dirname ${BASH_SOURCE[0]})/core.sh + +core.import logging +core.import exceptions +core.import arguments + +#region doc test setup +btrfs__doc_test_setup__=' +# is run once before tests are started +core.import doc_test +doc_test_capture_stderr=false +mv() { + echo mv $@ +} +rmdir() { + echo rmdir $@ +} +pv() { + cat - | tr -d "\n" # print stdin + echo -n " | pv | " +} +btrfs() { + if [[ $1 == subvolume ]] && [[ $2 == snapshot ]]; then + shift + shift + echo btrfs subvolume snapshot $@ + fi + if [[ $1 == send ]]; then + shift + echo btrfs send $@ + fi + if [[ $1 == receive ]]; then + cat - # print stdin + shift + echo btrfs receive $@ + fi + if [[ $1 == subvolume ]] && [[ $2 == list ]] && \ + [[ "${!#}" == /broot ]] + then + echo '\'' ID 256 parent 5 top level 5 path __active + ID 259 parent 256 top level 256 path __active/var + ID 258 parent 256 top level 256 path __active/usr + ID 257 parent 256 top level 256 path __active/home + ID 1661 parent 5 top level 5 path __snapshot/backup_last + ID 1662 parent 1661 top level 1661 path __snapshot/backup_last/var + ID 1663 parent 1661 top level 1661 path __snapshot/backup_last/usr + ID 1664 parent 1661 top level 1661 path __snapshot/backup_last/home'\'' + fi + if [[ $1 == subvolume ]] && [[ $2 == show ]]; then + if [[ $3 == /broot ]]; then + echo "Name: " + echo "UUID: 123456ab-abc1-2345" + return 0 + fi + # check if subvolume + [[ $3 == /broot/__active ]] && return 0 + [[ $3 == /broot/__active/var ]] && return 0 + [[ $3 == /broot/__active/usr ]] && return 0 + [[ $3 == /broot/__active/home ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last/var ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last/usr ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last/home ]] && return 0 + # return error if not a subvolume + return 1 + fi + if [[ $1 == subvolume ]] && [[ $2 == delete ]]; then + # check if subvolume + [[ $3 == /broot/__active ]] && return 0 + [[ $3 == /broot/__active/var ]] && return 0 + [[ $3 == /broot/__active/usr ]] && return 0 + [[ $3 == /broot/__active/home ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last/var ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last/usr ]] && return 0 + [[ $3 == /broot/__snapshot/backup_last/home ]] && return 0 + # return error if not a subvolume + return 1 + fi +} +' +#endregion + +# region helper functions +btrfs_is_subvolume() { + local __doc__=' + Checks if path is a subvolume. Note: The btrfs root is also a subvolume. + >>> btrfs_is_subvolume /broot; echo $? + 0 + >>> btrfs_is_subvolume /broot/__active; echo $? + 0 + >>> btrfs_is_subvolume /broot/__active/usr; echo $? + 0 + >>> btrfs_is_subvolume /broot/__active/etc; echo $? + 1 + ' + btrfs subvolume show "$1" &>/dev/null +} +btrfs_is_btrfs_root() { + local __doc__=' + >>> btrfs_is_btrfs_root /broot; echo $? + 0 + >>> btrfs_is_btrfs_root /broot/foo; echo $? + 1 + ' + #btrfs subvolume show "$1" 1>&2 + (btrfs subvolume show "$1" | grep "is btrfs root") &>/dev/null || \ + (btrfs subvolume show "$1" | grep "is toplevel") &>/dev/null || \ + (btrfs subvolume show "$1" | grep "Name:.*") &>/dev/null || \ + return 1 +} +btrfs_find_root() { + local __doc__=' + Returns absolute path to btrfs root. + Example: + >>> btrfs_find_root /broot/__active + /broot + >>> btrfs_find_root /broot/__snapshot/backup_last + /broot + >>> btrfs_find_root /not/a/valid/mountpoint; echo $? + 1 + ' + local path="$1" + while true; do + btrfs_is_btrfs_root "$path" && echo "$path" && return 0 + [[ "$path" == "/" ]] && return 1 + path="$(dirname "$path")" + done +} + +btrfs_get_subvolume_list_field() { + local __doc__=' + >>> local entry="$(btrfs subvolume list /broot | head -n1)" + >>> btrfs_get_subvolume_list_field path "$entry" + >>> btrfs_get_subvolume_list_field ID "$entry" + >>> btrfs_get_subvolume_list_field parent "$entry" + __active + 256 + 5 + ' + local target="$1" + local entry=($2) + local found=false + local field + for field in "${entry[@]}"; do + $found && echo "$field" && break + # case insensitive match (bash >= 4) + [[ "${field,,}" == "${target,,}" ]] && found=true + done +} +btrfs_subvolume_filter() { + local __doc__=' + Example: + >>> btrfs_subvolume_filter /broot parent 256 + ID 259 parent 256 top level 256 path __active/var + ID 258 parent 256 top level 256 path __active/usr + ID 257 parent 256 top level 256 path __active/home + >>> btrfs_subvolume_filter /broot id 256 + ID 256 parent 5 top level 5 path __active + ' + local btrfs_root="$(realpath "$1")" + local target_key="$2" + local target_value="$3" + local entry + btrfs_is_btrfs_root "$btrfs_root" || return 1 + btrfs subvolume list -p "$btrfs_root" | while read -r entry; do + local value + value="$(btrfs_get_subvolume_list_field "$target_key" "$entry")" + if [[ "$value" == "$target_value" ]]; then + echo "$entry" + fi + done +} +btrfs_get_child_volumes() { + # shellcheck disable=SC2016 + local __doc__=' + Returns absolute paths to subvolumes + Example: + >>> btrfs_get_child_volumes /broot/__active + /broot/__active/var + /broot/__active/usr + /broot/__active/home + >>> btrfs_get_child_volumes /broot/__snapshot/backup_last + /broot/__snapshot/backup_last/var + /broot/__snapshot/backup_last/usr + /broot/__snapshot/backup_last/home + ' + local volume="$1" + local btrfs_root entry volume_id volume_relative + btrfs_is_subvolume "${volume}" || return 1 + btrfs_root="$(btrfs_find_root "$volume")" + volume_relative="$(core.rel_path "$btrfs_root" "$volume")" + entry="$( + btrfs_subvolume_filter "$btrfs_root" path "$volume_relative" + )" + volume_id="$(btrfs_get_subvolume_list_field id "$entry")" + btrfs_subvolume_filter "$btrfs_root" parent "$volume_id" \ + | while read -r entry + do + child_path="$(btrfs_get_subvolume_list_field path "$entry")" + echo "${btrfs_root}/${child_path}" + done +} +# endregion + +#region btrfs functions +btrfs_subvolume_delete() { + local __doc__=' + # Delete a subvolume. Also deletes child subvolumes. + >>> btrfs_subvolume_delete /broot/__snapshot/backup_last + >>> echo $? + 0 + >>> btrfs_subvolume_delete /broot/__snapshot/foo + >>> echo $? + 1 + ' + local volume="$1" + local child + btrfs_subvolume_set_ro "$volume" false + btrfs_get_child_volumes "$volume" \ + | while read -r child + do + btrfs subvolume delete "$child" + done + btrfs subvolume delete "$volume" +} +btrfs_subvolume_set_ro() { + local __doc__=' + # Make subvolume writable or readonly. Also applies to child subvolumes. + ' + local volume="$1" + local read_only="$2" + [ -z "$2" ] && read_only=true + # if setting to writable set top volume first + $read_only || btrfs property set -ts "$volume" ro $read_only + local child + btrfs_get_child_volumes "$volume" | while read -r child; do + btrfs property set -ts "$child" ro $read_only + done + # if setting to read_only set top volume last + if $read_only; then + btrfs property set -ts "$volume" ro $read_only + fi +} +btrfs_snapshot() { + local __doc__=' + # Make snapshot of subvolume. + + >>> btrfs_snapshot /broot/__active /backup/__active_backup + btrfs subvolume snapshot /broot/__active /backup/__active_backup + rmdir /backup/__active_backup/var + btrfs subvolume snapshot /broot/__active/var /backup/__active_backup/var + rmdir /backup/__active_backup/usr + btrfs subvolume snapshot /broot/__active/usr /backup/__active_backup/usr + rmdir /backup/__active_backup/home + btrfs subvolume snapshot /broot/__active/home /backup/__active_backup/home + + Third parameter can be used to exclude a subvolume (currently only one) + >>> btrfs_snapshot /broot/__active /backup/__active_backup usr + btrfs subvolume snapshot /broot/__active /backup/__active_backup + rmdir /backup/__active_backup/var + btrfs subvolume snapshot /broot/__active/var /backup/__active_backup/var + rmdir /backup/__active_backup/home + btrfs subvolume snapshot /broot/__active/home /backup/__active_backup/home + ' + local volume="$1" + local target="$2" + local exclude="$3" + btrfs subvolume snapshot "${volume}" "${target}" + local child child_relative + btrfs_get_child_volumes "$volume" | while read -r child; do + child_relative="$(core.rel_path "$volume" "$child")" + if [ "$child_relative" != "$exclude" ]; then + rmdir "${target}/${child_relative}" + btrfs subvolume snapshot "${child}" "${target}/${child_relative}" + fi + done +} +btrfs_send_update() { + # shellcheck disable=SC2034,SC1004 + local __doc__=' + # Update snapshot (needs backing snapshot). + e.g + >>> btrfs_send_update /broot/__active \ + >>> /broot/backing \ + >>> /backup + btrfs send -p /broot/backing /broot/__active | pv | btrfs receive /backup + rmdir /backup/__active/var + btrfs send -p /broot/backing/var /broot/__active/var | pv | btrfs receive /backup/__active + rmdir /backup/__active/usr + btrfs send -p /broot/backing/usr /broot/__active/usr | pv | btrfs receive /backup/__active + rmdir /backup/__active/home + btrfs send -p /broot/backing/home /broot/__active/home | pv | btrfs receive /backup/__active + ' + local volume="$1" + local volume_name="$(basename "$1")" + local backing_snapshot="$2" + local target="$3" + # Note btrfs send can only operate on read-only snapshots + btrfs_subvolume_set_ro "$volume" true + btrfs_subvolume_set_ro "$backing_snapshot" true + btrfs send -p "$backing_snapshot" "$volume" | \ + pv --progress --timer --rate --average-rate --bytes | \ + btrfs receive "$target" + # Note btrfs receive can only create the subdirs if not read-only + btrfs_subvolume_set_ro "${target}/${volume_name}" false + local child child_relative + btrfs_get_child_volumes "$volume" | while read -r child; do + child_relative="$(core.rel_path "$volume" "$child")" + rmdir "${target}/${volume_name}/${child_relative}" + btrfs send -p "${backing_snapshot}/${child_relative}" "$child" | \ + pv --progress --timer --rate --average-rate --bytes | \ + btrfs receive "${target}/${volume_name}" + done + btrfs_subvolume_set_ro "$volume" false +} +btrfs_send() { + local __doc__=' + # Send snapshot + >>> btrfs_send /broot/__active /backup/__active_backup + btrfs send /broot/__active | pv | btrfs receive /backup + btrfs send /broot/__active/var | pv | btrfs receive /backup/__active + btrfs send /broot/__active/usr | pv | btrfs receive /backup/__active + btrfs send /broot/__active/home | pv | btrfs receive /backup/__active + mv /backup/__active /backup/__active_backup + ' + local volume="$1" + local volume_name="$(basename "$1")" + local target="$2" + local target_dir="$(dirname "$2")" + local target_name="$(basename "$2")" + # Note btrfs send can only operate on read-only snapshots + btrfs_subvolume_set_ro "$volume" true + btrfs send "$volume" | \ + pv --progress --timer --rate --average-rate --bytes | \ + btrfs receive "$target_dir" + # Note btrfs receive can only create the subdirs if not read-only + btrfs_subvolume_set_ro "${target_dir}/$volume_name" false + local child + btrfs_get_child_volumes "$volume" \ + | while read -r child + do + btrfs send "$child" | \ + pv --progress --timer --rate --average-rate --bytes | \ + btrfs receive "${target_dir}/${volume_name}" + done + mv "${target_dir}/$volume_name" "$target" + btrfs_subvolume_set_ro "$volume" false +} +#endregion + +# region vim modline +# vim: set tabstop=4 shiftwidth=4 expandtab: +# vim: foldmethod=marker foldmarker=region,endregion: +# endregion diff --git a/src/change_root.sh b/src/change_root.sh new file mode 100644 index 0000000..bc482da --- /dev/null +++ b/src/change_root.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +# region imports +source "$(dirname "${BASH_SOURCE[0]}")/core.sh" +core.import logging +# endregion + +change_root_kernel_api_locations=(/proc /sys /sys/firmware/efi/efivars /dev \ + /dev/pts /dev/shm /run) +# TODO implement dependency check in import mechanism +change_root__dependencies__=(mountpoint mount umount mkdir) +change_root__optional_dependencies__=(fakeroot fakechroot) + +change_root() { + local __doc__=' + This function performs a linux change root if needed and provides all + kernel api filesystems in target root by using a change root interface + with minimal needed rights. + + #### Example: + + `change_root /new_root /usr/bin/env bash some arguments` + ' + if [[ "$1" == '/' ]]; then + shift + return $? + else + change_root_with_kernel_api "$@" + return $? + fi + return $? +} + +change_root_with_fake_fallback() { + local __doc__=' + Perform the available change root program wich needs at least rights. + + #### Example: + + `change_root_with_fake_fallback /new_root /usr/bin/env bash some arguments` + ' + if [[ "$UID" == '0' ]]; then + chroot "$@" + return $? + fi + fakeroot fakechroot chroot "$@" + return $? +} + +change_root_with_kernel_api() { + local __doc__=' + Performs a change root by mounting needed host locations in change root + environment. + + #### Example: + + `change_root_with_kernel_api /new_root /usr/bin/env bash some arguments` + ' + local new_root_location="$1" + if [[ ! "$new_root_location" =~ .*/$ ]]; then + new_root_location+='/' + fi + local mountpoint_path + for mountpoint_path in ${change_root_kernel_api_locations[*]}; do + mountpoint_path="${mountpoint_path:1}" + # TODO fix + #./build-initramfs.sh -d -p ../../initramfs -s -t /mnt/old + #mkdir: cannot create directory ‘/mnt/old/sys/firmware/efi’: No such file or directory + #Traceback (most recent call first): + #[0] /srv/openslx-ng/systemd-init/builder/dnbd3-rootfs/scripts/rebash/change_root.sh:67: change_root_with_kernel_api + #[1] /srv/openslx-ng/systemd-init/builder/dnbd3-rootfs/scripts/rebash/change_root.sh:28: change_root + #[2] ./build-initramfs.sh:532: main + #[3] ./build-initramfs.sh:625: main + if [ ! -e "${new_root_location}${mountpoint_path}" ]; then + mkdir --parents "${new_root_location}${mountpoint_path}" + # TODO remember created dirs. + fi + if ! mountpoint -q "${new_root_location}${mountpoint_path}"; then + if [ "$mountpoint_path" == 'proc' ]; then + mount "/${mountpoint_path}" \ + "${new_root_location}${mountpoint_path}" --types \ + "$mountpoint_path" --options nosuid,noexec,nodev + elif [ "$mountpoint_path" == 'sys' ]; then + mount "/${mountpoint_path}" \ + "${new_root_location}${mountpoint_path}" --types sysfs \ + --options nosuid,noexec,nodev + elif [ "$mountpoint_path" == 'dev' ]; then + mount udev "${new_root_location}${mountpoint_path}" --types \ + devtmpfs --options mode=0755,nosuid + elif [ "$mountpoint_path" == 'dev/pts' ]; then + mount devpts "${new_root_location}${mountpoint_path}" \ + --types devpts --options mode=0620,gid=5,nosuid,noexec + elif [ "$mountpoint_path" == 'dev/shm' ]; then + mount shm "${new_root_location}${mountpoint_path}" --types \ + tmpfs --options mode=1777,nosuid,nodev + elif [ "$mountpoint_path" == 'run' ]; then + mount "/${mountpoint_path}" \ + "${new_root_location}${mountpoint_path}" --types tmpfs \ + --options nosuid,nodev,mode=0755 + elif [ "$mountpoint_path" == 'tmp' ]; then + mount run "${new_root_location}${mountpoint_path}" --types \ + tmpfs --options mode=1777,strictatime,nodev,nosuid + elif [ -f "/${mountpoint_path}" ]; then + mount "/${mountpoint_path}" \ + "${new_root_location}${mountpoint_path}" --bind + else + logging.warn \ + "Mountpoint \"/${mountpoint_path}\" couldn't be handled." + fi + fi + done + change_root_with_fake_fallback "$@" + local return_code=$? + # Reverse mountpoint list to unmount them in reverse order. + local reverse_kernel_api_locations + for mountpoint_path in ${reverse_kernel_api_locations[*]}; do + reverse_kernel_api_locations="$mountpoint_path ${reverse_kernel_api_locations[*]}" + done + for mountpoint_path in ${reverse_kernel_api_locations[*]}; do + mountpoint_path="${mountpoint_path:1}" && \ + if mountpoint -q "${new_root_location}${mountpoint_path}" || \ + [ -f "/${mountpoint_path}" ] + then + # If unmounting doesn't work try to unmount in lazy mode (when + # mountpoints are not needed anymore). + if ! umount "${new_root_location}${mountpoint_path}"; then + logging.warn "Unmounting \"${new_root_location}${mountpoint_path}\" fails so unmount it in force mode." + if ! umount -f "${new_root_location}${mountpoint_path}"; then + logging.warn "Unmounting \"${new_root_location}${mountpoint_path}\" in force mode fails so unmount it if mountpoint isn't busy anymore." + umount -l "${new_root_location}${mountpoint_path}" + fi + fi + # NOTE: "return_code" remains with an error code if there was + # given one in all iterations. + [[ $? != 0 ]] && return_code=$? + else + logging.warn \ + "Location \"${new_root_location}${mountpoint_path}\" should be a mountpoint but isn't." + fi + done + return $return_code +} + +alias change_root.kernel_api_locations=change_root_kernel_api_locations +alias change_root.with_fake_fallback=change_root_with_fake_fallback +alias change_root.with_kernel_api=change_root_with_kernel_api diff --git a/src/core.sh b/src/core.sh new file mode 100644 index 0000000..baaf96b --- /dev/null +++ b/src/core.sh @@ -0,0 +1,406 @@ +#!/usr/bin/env bash +if [ ${#core_imported_modules[@]} -ne 0 ]; then + # load core only once + return 0 +fi + +shopt -s expand_aliases +#TODO use set -o nounset + +core_is_main() { + local __doc__=' + Returns true if current script is being executed. + + >>> # Note: this test passes because is_main is called by doc_test.sh which + >>> # is being executed. + >>> core.is_main && echo yes + yes + ' + [[ "${BASH_SOURCE[1]}" = "$0" ]] +} +core_abs_path() { + local path="$1" + if [ -d "$path" ]; then + local abs_path_dir + abs_path_dir="$(cd "$path" && pwd)" + echo "${abs_path_dir}" + else + local file_name + local abs_path_dir + file_name="$(basename "$path")" + path=$(dirname "$path") + abs_path_dir="$(cd "$path" && pwd)" + echo "${abs_path_dir}/${file_name}" + fi +} +core_rel_path() { + # shellcheck disable=SC2016 + local __doc__=' + Computes relative path from $1 to $2. + Taken from http://stackoverflow.com/a/12498485/2972353 + + >>> core_rel_path "/A/B/C" "/A" + ../.. + >>> core_rel_path "/A/B/C" "/A/B" + .. + >>> core_rel_path "/A/B/C" "/A/B/C/D" + D + >>> core_rel_path "/A/B/C" "/A/B/C/D/E" + D/E + >>> core_rel_path "/A/B/C" "/A/B/D" + ../D + >>> core_rel_path "/A/B/C" "/A/B/D/E" + ../D/E + >>> core_rel_path "/A/B/C" "/A/D" + ../../D + >>> core_rel_path "/A/B/C" "/A/D/E" + ../../D/E + >>> core_rel_path "/A/B/C" "/D/E/F" + ../../../D/E/F + >>> core_rel_path "/" "/" + . + >>> core_rel_path "/A/B/C" "/A/B/C" + . + >>> core_rel_path "/A/B/C" "/" + ../../../ + ' + # both $1 and $2 are absolute paths beginning with / + # returns relative path to $2/$target from $1/$source + local source="$1" + local target="$2" + if [[ "$source" == "$target" ]]; then + echo "." + return + fi + + local common_part="$source" # for now + local result="" # for now + + while [[ "${target#$common_part}" == "${target}" ]]; do + # no match, means that candidate common part is not correct + # go up one level (reduce common part) + common_part="$(dirname "$common_part")" + # and record that we went back, with correct / handling + if [[ -z $result ]]; then + result=".." + else + result="../$result" + fi + done + + if [[ $common_part == "/" ]]; then + # special case for root (no common path) + result="$result/" + fi + + # since we now have identified the common part, + # compute the non-common part + local forward_part="${target#$common_part}" + + # and now stick all parts together + if [[ -n $result ]] && [[ -n $forward_part ]]; then + result="$result$forward_part" + elif [[ -n $forward_part ]]; then + # extra slash removal + result="${forward_part:1}" + fi + echo "$result" +} + +core_imported_modules=("$(core_abs_path "${BASH_SOURCE[0]}")") +core_imported_modules+=("$(core_abs_path "${BASH_SOURCE[1]}")") +core_declarations_before="" +core_declared_functions_after_import="" +core_import_level=0 + +core_log() { + if type -t logging_log > /dev/null; then + logging_log "$@" + else + local level=$1 + shift + echo "$level": "$@" + fi +} +core_is_empty() { + local __doc__=' + Tests if variable is empty (undefined variables are not empty) + + >>> local foo="bar" + >>> core_is_empty foo; echo $? + 1 + >>> local defined_and_empty="" + >>> core_is_empty defined_and_empty; echo $? + 0 + >>> core_is_empty undefined_variable; echo $? + 1 + + >>> set -u + >>> core_is_empty undefined_variable; echo $? + 1 + ' + local variable_name="$1" + core_is_defined "$variable_name" || return 1 + [ -z "${!variable_name}" ] || return 1 +} +core_is_defined() { + # shellcheck disable=SC2034 + local __doc__=' + Tests if variable is defined (can also be empty) + + >>> local foo="bar" + >>> core_is_defined foo; echo $? + >>> [[ -v foo ]]; echo $? + 0 + 0 + >>> local defined_but_empty="" + >>> core_is_defined defined_but_empty; echo $? + 0 + >>> core_is_defined undefined_variable; echo $? + 1 + >>> set -o nounset + >>> core_is_defined undefined_variable; echo $? + 1 + + Same Tests for bash < 4.3 + >>> core__bash_version_test=true + >>> local foo="bar" + >>> core_is_defined foo; echo $? + 0 + >>> core__bash_version_test=true + >>> local defined_but_empty="" + >>> core_is_defined defined_but_empty; echo $? + 0 + >>> core__bash_version_test=true + >>> core_is_defined undefined_variable; echo $? + 1 + >>> core__bash_version_test=true + >>> set -o nounset + >>> core_is_defined undefined_variable; echo $? + 1 + ' + ( + set +o nounset + if ((BASH_VERSINFO[0] >= 4)) && ((BASH_VERSINFO[1] >= 3)) \ + && [ -z "${core__bash_version_test:-}" ]; then + [[ -v "${1:-}" ]] || exit 1 + else # for bash < 4.3 + # Note: ${varname:-foo} expands to foo if varname is unset or set to the + # empty string; ${varname-foo} only expands to foo if varname is unset. + # shellcheck disable=SC2016 + eval '! [[ "${'"${1}"'-this_variable_is_undefined_!!!}"' \ + ' == "this_variable_is_undefined_!!!" ]]' + exit $? + fi + ) +} +core_get_all_declared_names() { + # shellcheck disable=SC2016 + local __doc__=' + Return all declared variables and function in the current scope. + + E.g. + `declarations="$(core.get_all_declared_names)"` + ' + local only_functions="${1:-}" + [ -z "$only_functions" ] && only_functions=false + { + declare -F | cut --delimiter ' ' --fields 3 + $only_functions || declare -p | grep '^declare' \ + | cut --delimiter ' ' --fields 3 - | cut --delimiter '=' --fields 1 + } | sort --unique +} +core_get_all_aliases() { + local __doc__=' + Returns all defined aliases in the current scope. + ' + alias | grep '^alias' \ + | cut --delimiter ' ' --fields 2 - | cut --delimiter '=' --fields 1 +} +core_source_with_namespace_check() { + local __doc__=' + Sources a script and checks variable definitions before and after sourcing. + ' + # TODO make sure sourcing a file does not change the value of already + # defined variables. + local module_path="$1" + local namespace="$2" + local declarations_after declarations_diff + [ "$core_import_level" = '0' ] && \ + core_declared_functions_before="$(mktemp --suffix=rebash-core-before)" + core_get_all_declared_names true > "$core_declared_functions_before" + declarations_after="$(mktemp --suffix=rebash-core-dec-after)" + if [ "$core_declarations_before" = "" ]; then + core_declarations_before="$(mktemp --suffix=rebash-core-dec)" + fi + # region check if namespace clean before sourcing + local variable_or_function core_variable + core_get_all_declared_names > "$core_declarations_before" + while read -r variable_or_function ; do + if [[ $variable_or_function =~ ^${namespace}[._]* ]]; then + core_log warn "Namespace '$namespace' is not clean:" \ + "'$variable_or_function' is defined" 1>&2 + fi + done < "$core_declarations_before" + # endregion + + core_import_level=$((core_import_level+1)) + # shellcheck disable=1090 + source "$module_path" + [ $? = 1 ] && core_log critical "Failed to source $module_path" && exit 1 + core_import_level=$((core_import_level-1)) + + # check if sourcing defined unprefixed names + core_get_all_declared_names > "$declarations_after" + if ! $core_suppress_declaration_warning; then + declarations_diff="$(! diff "$core_declarations_before" \ + "$declarations_after" | grep -e "^>" | sed 's/^> //')" + for variable_or_function in $declarations_diff; do + if ! [[ $variable_or_function =~ ^${namespace}[._]* ]]; then + core_log warn "module \"$namespace\" defines unprefixed" \ + "name: \"$variable_or_function\"" 1>&2 + fi + done + fi + core_get_all_declared_names > "$core_declarations_before" + if [ "$core_import_level" = '0' ]; then + rm "$core_declarations_before" + core_declarations_before="" + core_declared_functions_after="$(mktemp --suffix=rebash-core-after)" + core_get_all_declared_names true > "$core_declared_functions_after" + core_declared_functions_after_import="$(! diff \ + "$core_declared_functions_before" \ + "$core_declared_functions_after" \ + | grep '^>' | sed 's/^> //' + )" + rm "$core_declared_functions_after" + rm "$core_declared_functions_before" + fi + if (( core_import_level == 1 )); then + declare -F | cut --delimiter ' ' --fields 3 \ + > "$core_declared_functions_before" + fi + rm "$declarations_after" +} +core_suppress_declaration_warning=false +core_import() { + # shellcheck disable=SC2016,SC1004 + local __doc__=' + IMPORTANT: Do not use core.import inside functions -> aliases do not work + TODO: explain this in more detail + + >>> ( + >>> core.import logging + >>> logging_set_level warn + >>> core.import ../test/mockup_module-b.sh false + >>> ) + +doc_test_contains + imported module c + module "mockup_module_c" defines unprefixed name: "foo123" + imported module b + + Modules should be imported only once. + >>> (core.import ../test/mockup_module_a.sh && \ + >>> core.import ../test/mockup_module_a.sh) + imported module a + + >>> ( + >>> core.import ../test/mockup_module_a.sh false + >>> echo $core_declared_functions_after_import + >>> ) + imported module a + mockup_module_a_foo + + >>> ( + >>> core.import logging + >>> logging_set_level warn + >>> core.import ../test/mockup_module_c.sh false + >>> echo $core_declared_functions_after_import + >>> ) + +doc_test_contains + imported module b + imported module c + module "mockup_module_c" defines unprefixed name: "foo123" + foo123 + + ' + local module="$1" + local suppress_declaration_warning="${2:-}" + # If "$suppress_declaration_warning" is empty do not change the current value + # of "$core_suppress_declaration_warning". (So it is not changed by nested + # imports.) + if [[ "$suppress_declaration_warning" == "true" ]]; then + core_suppress_declaration_warning=true + elif [[ "$suppress_declaration_warning" == "false" ]]; then + core_suppress_declaration_warning=false + fi + local module_path="" + local path + # shellcheck disable=SC2034 + core_declared_functions_after_import="" + + path="$(core_abs_path "$(dirname "${BASH_SOURCE[0]}")")" + local caller_path + caller_path="$(core_abs_path "$(dirname "${BASH_SOURCE[1]}")")" + # try absolute + if [[ $module == /* ]] && [[ -e "$module" ]];then + module_path="$module" + fi + # try relative + if [[ -f "${caller_path}/${module}" ]]; then + module_path="${caller_path}/${module}" + fi + # try rebash modules + if [[ -f "${path}/${module%.sh}.sh" ]]; then + module_path="${path}/${module%.sh}.sh" + fi + + if [ "$module_path" == "" ]; then + core_log critical "failed to import \"$module\"" + return 1 + fi + + module="$(basename "$module_path")" + + # normalize module_path + module_path="$(core.abs_path "$module_path")" + # check if module already loaded + local loaded_module + for loaded_module in "${core_imported_modules[@]}"; do + if [[ "$loaded_module" == "$module_path" ]];then + (( core_import_level == 0 )) && \ + core_declarations_before='' + return 0 + fi + done + + core_imported_modules+=("$module_path") + core_source_with_namespace_check "$module_path" "${module%.sh}" +} +core_unique() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + >>> local foo="a\nb\na\nb\nc\nb\nc" + >>> echo -e "$foo" | core.unique + a + b + c + ' + nl "$@" | sort --key 2 | uniq --skip-fields 1 | sort --numeric-sort | \ + sed 's/\s*[0-9]\+\s\+//' +} +alias core.import="core_import" +alias core.abs_path="core_abs_path" +alias core.rel_path="core_rel_path" +alias core.is_main="core_is_main" +alias core.get_all_declared_names="core_get_all_declared_names" +alias core.unique="core_unique" +alias core.is_defined="core_is_defined" +alias core.is_empty="core_is_empty" + +# region vim modline + +# vim: set tabstop=4 shiftwidth=4 expandtab: +# vim: foldmethod=marker foldmarker=region,endregion: + +# endregion diff --git a/src/dictionary.sh b/src/dictionary.sh new file mode 100644 index 0000000..b0c66e9 --- /dev/null +++ b/src/dictionary.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source "$(dirname "${BASH_SOURCE[0]}")/core.sh" +core.import logging + +dictionary_set() { + # shellcheck disable=SC2016 + local __doc__=' + Usage: `dictionary.set dictionary_name key value` + + #### Tests + + >>> dictionary_set map foo 2 + >>> echo ${dictionary__store_map[foo]} + 2 + >>> dictionary_set map foo "a b c" bar 5 + >>> echo ${dictionary__store_map[foo]} + >>> echo ${dictionary__store_map[bar]} + a b c + 5 + >>> dictionary_set map foo "a b c" bar; echo $? + 1 + + >>> dictionary__bash_version_test=true + >>> dictionary_set map foo 2 + >>> echo $dictionary__store_map_foo + 2 + >>> dictionary__bash_version_test=true + >>> dictionary_set map foo "a b c" + >>> echo $dictionary__store_map_foo + a b c + ' + local name="$1" + while true; do + local key="$2" + local value="\"$3\"" + shift 2 + (( $# % 2 )) || return 1 + # shellcheck disable=SC2154 + if [[ ${BASH_VERSINFO[0]} -lt 4 ]] \ + || ! [ -z "${dictionary__bash_version_test:-}" ]; then + eval "dictionary__store_${name}_${key}=""$value" + else + declare -Ag "dictionary__store_${name}" + eval "dictionary__store_${name}[${key}]=""$value" + fi + (( $# == 1 )) && return + done +} +dictionary_get_keys() { + local __doc__=' + Get keys of a dictionary as array. + + Usage: `dictionary.get_keys dictionary_name` + + + >>> dictionary_set map foo "a b c" bar 5 + >>> dictionary_get_keys map + foo + bar + + Iterate keys: + >>> dictionary_set map foo "a b c" bar 5 + >>> local key + >>> for key in $(dictionary_get_keys map); do + >>> echo "$key": "$(dictionary_get map "$key")" + >>> done + foo: a b c + bar: 5 + + >>> dictionary__bash_version_test=true + >>> dictionary_set map foo "a b c" bar 5 + >>> dictionary_get_keys map | sort -u + bar + foo + ' + local name="$1" + local keys key + local store='dictionary__store_'"${name}" + if [[ ${BASH_VERSINFO[0]} -lt 4 ]] \ + || ! [ -z "${dictionary__bash_version_test:-}" ]; then + for key in $(declare -p | cut -d' ' -f3 \ + | grep -E "^${store}" | cut -d '=' -f1); do + echo "${key#${store}_}" + done + else + # shellcheck disable=SC2016 + eval 'keys="${!'"$store"'[@]}"' + fi + # shellcheck disable=SC2154 + for key in ${keys:-}; do + echo "$key" + done +} +dictionary_get() { + # shellcheck disable=SC2034,2016 + local __doc__=' + Usage: `variable=$(dictionary.get dictionary_name key)` + + #### Examples + + >>> dictionary_get unset_map unset_value; echo $? + 1 + >>> dictionary__bash_version_test=true + >>> dictionary_get unset_map unset_value; echo $? + 1 + + >>> dictionary_set map foo 2 + >>> dictionary_set map bar 1 + >>> dictionary_get map foo + >>> dictionary_get map bar + 2 + 1 + + >>> dictionary_set map foo "a b c" + >>> dictionary_get map foo + a b c + + >>> dictionary__bash_version_test=true + >>> dictionary_set map foo 2 + >>> dictionary_get map foo + 2 + + >>> dictionary__bash_version_test=true + >>> dictionary_set map foo "a b c" + >>> dictionary_get map foo + a b c + ' + local name="$1" + local key="$2" + if [[ ${BASH_VERSINFO[0]} -lt 4 ]] \ + || ! [ -z "${dictionary__bash_version_test:-}" ]; then + local store="dictionary__store_${name}_${key}" + else + local store="dictionary__store_${name}[${key}]" + fi + core_is_defined "${store}" || return 1 + local value="${!store}" + echo "$value" +} +alias dictionary.set='dictionary_set' +alias dictionary.get='dictionary_get' +alias dictionary.get_keys='dictionary_get_keys' diff --git a/src/doc_test.sh b/src/doc_test.sh new file mode 100755 index 0000000..70ea362 --- /dev/null +++ b/src/doc_test.sh @@ -0,0 +1,707 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source "$(dirname "$(readlink --canonicalize "${BASH_SOURCE[0]}")")/core.sh" + +core.import logging +core.import ui +core.import exceptions +core.import utils +core.import arguments +core.import time +core.import documentation +core.import utils +# region doc +# shellcheck disable=SC2034,SC2016 +doc_test__doc__=' + The doc_test module implements function and module level testing via "doc + strings". + + Tests can be run by invoking `doc_test.sh file1 folder1 file2 ...`. + + #### Options: + ``` + --help|-h Print help message. + --side-by-side Print diff of failing tests side by side. + --no-check-namespace Do not warn about unprefixed definitions. + --no-check-undocumented Do not warn about undocumented functions. + --use-nounset Accessing undefined variables produces error. + --verbose|-v Be more verbose + ``` + + #### Example output `./doc_test.sh -v arguments.sh` + ```bash + [verbose:doc_test.sh:330] arguments:[PASS] + [verbose:doc_test.sh:330] arguments_get_flag:[PASS] + [verbose:doc_test.sh:330] arguments_get_keyword:[PASS] + [verbose:doc_test.sh:330] arguments_get_parameter:[PASS] + [verbose:doc_test.sh:330] arguments_get_positional:[PASS] + [verbose:doc_test.sh:330] arguments_set:[PASS] + [info:doc_test.sh:590] arguments - passed 6/6 tests in 918 ms + [info:doc_test.sh:643] Total: passed 1/1 modules in 941 ms + ``` + + A doc string can be defined for a function by defining a variable named + `__doc__` at the function scope. + On the module level, the variable name should be `__doc__` + (e.g. `arguments__doc__` for the example above). + Note: The doc string needs to be defined with single quotes. + + Code contained in a module level variable named + `__doc_test_setup__` will be run once before all the Tests of + a module are run. This is usefull for defining mockup functions/data + that can be used throughout all tests. + + +documentation_exclude_print + #### Tests + + Tests are delimited by blank lines: + >>> echo bar + bar + + >>> echo $(( 1 + 2 )) + 3 + + But can also occur right after another: + >>> echo foo + foo + >>> echo bar + bar + + Single quotes can be escaped like so: + >>> echo '"'"'$foos'"'"' + $foos + + Or so + >>> echo '\''$foos'\'' + $foos + + Some text in between. + + Multiline output + >>> local i + >>> for i in 1 2; do + >>> echo $i; + >>> done + 1 + 2 + + Ellipsis support + >>> local i + >>> for i in 1 2 3 4 5; do + >>> echo $i; + >>> done + +doc_test_ellipsis + 1 + 2 + ... + + Ellipsis are non greedy + >>> local i + >>> for i in 1 2 3 4 5; do + >>> echo $i; + >>> done + +doc_test_ellipsis + 1 + ... + 4 + 5 + + Each testcase has its own scope: + >>> local testing="foo"; echo $testing + foo + >>> [ -z "${testing:-}" ] && echo empty + empty + + Syntax error in testcode: + >>> f() {a} + +doc_test_contains + +doc_test_ellipsis + syntax error near unexpected token `{a} + ... + + -documentation_exclude_print +' +# endregion +doc_test_compare_result() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + >>> local buffer="line 1 + >>> line 2" + >>> local got="line 1 + >>> line 2" + >>> doc_test_compare_result "$buffer" "$got"; echo $? + 0 + >>> local buffer="line 1 + >>> foo" + >>> local got="line 1 + >>> line 2" + >>> doc_test_compare_result "$buffer" "$got"; echo $? + 1 + >>> local buffer="+doc_test_contains + >>> line + >>> line" + >>> local got="line 1 + >>> line 2" + >>> doc_test_compare_result "$buffer" "$got"; echo $? + 0 + >>> local buffer="+doc_test_contains + >>> line + >>> foo" + >>> local got="line 1 + >>> line 2" + >>> doc_test_compare_result "$buffer" "$got"; echo $? + 1 + >>> local buffer="+doc_test_ellipsis + >>> line + >>> ... + >>> " + >>> local got="line + >>> line 2 + >>> " + >>> doc_test_compare_result "$buffer" "$got"; echo $? + 0 + >>> local buffer="+doc_test_ellipsis + >>> line + >>> ... + >>> line 2 + >>> " + >>> local got="line + >>> ignore + >>> ignore + >>> line 2 + >>> " + >>> doc_test_compare_result "$buffer" "$got"; echo $? + 0 + >>> local buffer="+doc_test_ellipsis + >>> line + >>> ... + >>> line 2 + >>> " + >>> local got="line + >>> ignore + >>> ignore + >>> line 2 + >>> line 3 + >>> " + >>> doc_test_compare_result "$buffer" "$got"; echo $? + 1 + ' + local buffer="$1" + local got="$2" + local buffer_line + local got_line + doc_test_compare_lines () { + if $doc_test_contains; then + [[ "$got_line" == *"$buffer_line"* ]] || return 1 + else + [[ "$buffer_line" == "$got_line" ]] || return 1 + fi + } + local result=0 + local doc_test_contains=false + local doc_test_ellipsis=false + local doc_test_ellipsis_on=false + local doc_test_ellipsis_waiting=false + local end_of_buffer=false + local end_of_got=false + while true; do + # parse buffer line + if ! $doc_test_ellipsis_waiting && ! $end_of_buffer && ! read -r -u3 buffer_line; then + end_of_buffer=true + fi + if [[ "$buffer_line" == "+doc_test_no_capture_stderr"* ]]; then + continue + fi + if [[ "$buffer_line" == "+doc_test_contains"* ]]; then + doc_test_contains=true + continue + fi + if [[ "$buffer_line" == "+doc_test_ellipsis"* ]]; then + doc_test_ellipsis=true + continue + fi + # parse got line + if $end_of_got || ! read -r -u4 got_line; then + end_of_got=true + fi + + # set result + if $doc_test_ellipsis;then + if [[ "$buffer_line" == "..." ]]; then + doc_test_ellipsis_on=true + else + [[ "$buffer_line" != "" ]] && $doc_test_ellipsis_on && doc_test_ellipsis_waiting=true + fi + fi + $end_of_buffer && $end_of_got && break + $end_of_buffer && $doc_test_ellipsis_waiting && result=1 && break + $end_of_got && $doc_test_ellipsis_waiting && result=1 && break + $end_of_buffer && $doc_test_ellipsis_on && break + if $doc_test_ellipsis_on; then + if doc_test_compare_lines; then + doc_test_ellipsis_on=false + doc_test_ellipsis_waiting=false + else + $end_of_got && result=1 + fi + else + doc_test_compare_lines || result=1 + fi + done 3<<< "$buffer" 4<<< "$got" + return $result +} +# shellcheck disable=SC2154 +doc_test_eval() { + local __doc__=' + >>> local test_buffer=" + >>> echo foo + >>> echo bar + >>> " + >>> local output_buffer="foo + >>> bar" + >>> doc_test_use_side_by_side_output=false + >>> doc_test_module_under_test=core + >>> doc_test_nounset=false + >>> doc_test_eval "$test_buffer" "$output_buffer" + ' + local test_buffer="$1" + [[ -z "$test_buffer" ]] && return 0 + local output_buffer="$2" + local text_buffer="${3-}" + local module="${4-}" + local function="${5-}" + local result=0 + local got declarations_before declarations_after + doc_test_eval_with_check() { + local test_buffer="$1" + local module="$2" + local function="$3" + local core_path="$(core_abs_path "$(dirname "${BASH_SOURCE[0]}")")/core.sh" + local setup_identifier="${module//[^[:alnum:]_]/_}"__doc_test_setup__ + local setup_string="${!setup_identifier:-}" + test_script="$( + echo '[ -z "$BASH_REMATCH" ] && BASH_REMATCH=""' + echo "source $core_path" + # Suppress the warnings here because they have been already been + # printed when analyzing the whole module + echo "core.import $doc_test_module_under_test true" + echo "$setup_string" + # _ can be used as anonymous variable (without warning) + echo '_=""' + echo "core.get_all_declared_names > $declarations_before" + $doc_test_nounset && echo 'set -o nounset' + # wrap in a function so the "local" keyword has an effect inside + # tests + echo " + _() { + $test_buffer + } + _ + " + echo "core.get_all_declared_names > $declarations_after" + )" + # run in clean environment + if echo "$output_buffer" | grep '+doc_test_no_capture_stderr' &>/dev/null; + then + #(eval "$test_script") + bash --noprofile --norc <(echo "$test_script") + else + #(eval "$test_script" 2>&1) + bash --noprofile --norc 2>&1 <(echo "$test_script") + fi + local result=$? + return $result + } + declarations_before="$(mktemp --suffix=rebash-doc_test)" + trap "rm -f $declarations_before; exit" EXIT + declarations_after="$(mktemp --suffix=rebash-doc_test)" + trap "rm -f $declarations_after; exit" EXIT + # TODO $module $function as parameters + got="$(doc_test_eval_with_check "$test_buffer" "$module" "$function")" + doc_test_declarations_diff="$(diff "$declarations_before" "$declarations_after" \ + | grep -e "^>" | sed 's/^> //')" + # TODO $module $function as parameters + doc_test_print_declaration_warning "$module" "$function" + rm "$declarations_before" + rm "$declarations_after" + if ! doc_test_compare_result "$output_buffer" "$got"; then + echo -e "${ui_color_lightred}test:${ui_color_default}" + echo "$test_buffer" + if $doc_test_use_side_by_side_output; then + output_buffer="expected"$'\n'"${output_buffer}" + got="got"$'\n'"${got}" + # TODO exclude doc_test_options + local diff=diff + utils.dependency_check colordiff && diff=colordiff + $diff --side-by-side <(echo "$output_buffer") <(echo "$got") + else + echo -e "${ui_color_lightred}expected:${ui_color_default}" + echo "$output_buffer" + echo -e "${ui_color_lightred}got:${ui_color_default}" + echo "$got" + fi + return 1 + fi +} +doc_test_run_test() { + local doc_string="$1" + local module="$2" + local function="$3" + local test_name="$module" + [[ -z "$function" ]] || test_name="$function" + if doc_test_parse_doc_string "$doc_string" doc_test_eval ">>>" \ + "$module" "$function" + then + logging.verbose "$test_name:[${ui_color_lightgreen}PASS${ui_color_default}]" + else + logging.warn "$test_name:[${ui_color_lightred}FAIL${ui_color_default}]" + return 1 + fi +} +doc_test_parse_doc_string() { + local __doc__=' + >>> local doc_string=" + >>> (test)block + >>> output block + >>> " + >>> _() { + >>> local output_buffer="$2" + >>> echo block: + >>> while read -r line; do + >>> if [ -z "$line" ]; then + >>> echo "empty_line" + >>> else + >>> echo "$line" + >>> fi + >>> done <<< "$output_buffer" + >>> } + >>> doc_test_parse_doc_string "$doc_string" _ "(test)" + block: + output block + + >>> local doc_string=" + >>> Some text (block 1). + >>> + >>> + >>> Some more text (block 1). + >>> (test)block 2 + >>> (test)block 2.2 + >>> output block 2 + >>> (test)block 3 + >>> output block 3 + >>> + >>> Even more text (block 4). + >>> " + >>> local i=0 + >>> _() { + >>> local test_buffer="$1" + >>> local output_buffer="$2" + >>> local text_buffer="$3" + >>> local line + >>> (( i++ )) + >>> echo "text_buffer (block $i):" + >>> if [ ! -z "$text_buffer" ]; then + >>> while read -r line; do + >>> if [ -z "$line" ]; then + >>> echo "empty_line" + >>> else + >>> echo "$line" + >>> fi + >>> done <<< "$text_buffer" + >>> fi + >>> echo "test_buffer (block $i):" + >>> [ ! -z "$test_buffer" ] && echo "$test_buffer" + >>> echo "output_buffer (block $i):" + >>> [ ! -z "$output_buffer" ] && echo "$output_buffer" + >>> return 0 + >>> } + >>> doc_test_parse_doc_string "$doc_string" _ "(test)" + text_buffer (block 1): + Some text (block 1). + empty_line + empty_line + Some more text (block 1). + test_buffer (block 1): + output_buffer (block 1): + text_buffer (block 2): + test_buffer (block 2): + block 2 + block 2.2 + output_buffer (block 2): + output block 2 + text_buffer (block 3): + test_buffer (block 3): + block 3 + output_buffer (block 3): + output block 3 + text_buffer (block 4): + Even more text (block 4). + test_buffer (block 4): + output_buffer (block 4): + + ' + local preserve_prompt + arguments.set "$@" + arguments.get_flag --preserve-prompt preserve_prompt + arguments.apply_new_arguments + local doc_string="$1" # the docstring to test + local parse_buffers_function="$2" + local prompt="$3" + local module="${4:-}" + local function="${5:-}" + [ -z "$prompt" ] && prompt=">>>" + local text_buffer="" + local test_buffer="" + local output_buffer="" + + # remove leading blank line + [[ "$(head --lines=1 <<< "$doc_string")" != *[![:space:]]* ]] && + doc_string="$(tail --lines=+2 <<< "$doc_string" )" + # remove trailing blank line + [[ "$(tail --lines=1 <<< "$doc_string")" != *[![:space:]]* ]] && + doc_string="$(head --lines=-1 <<< "$doc_string" )" + + doc_test_eval_buffers() { + $parse_buffers_function "$test_buffer" "$output_buffer" \ + "$text_buffer" "$module" "$function" + local result=$? + # clear buffers + text_buffer="" + test_buffer="" + output_buffer="" + return $result + } + local line + local state=TEXT + local next_state + local temp_prompt + #local indentation="" + while read -r line; do + #line="$(echo "$line" | sed -e 's/^[[:blank:]]*//')" # lstrip + case "$state" in + TEXT) + if [[ "$line" = "" ]]; then + next_state=TEXT + [ ! -z "$text_buffer" ] && text_buffer+=$'\n'"$line" + elif [[ "$line" = "$prompt"* ]]; then + next_state=TEST + [ ! -z "$text_buffer" ] && doc_test_eval_buffers + $preserve_prompt && temp_prompt="$prompt" && prompt="" + test_buffer="${line#$prompt}" + $preserve_prompt && prompt="$temp_prompt" + else + next_state=TEXT + # check if start of text + if [ -z "$text_buffer" ]; then + text_buffer="$line" + else + text_buffer+=$'\n'"$line" + fi + fi + ;; + TEST) + #[ -z "$indentation" ] && + #indentation="$(echo "$line"| grep -o "^[[:blank:]]*")" + if [[ "$line" = "" ]]; then + next_state=TEXT + doc_test_eval_buffers + [ $? == 1 ] && return 1 + elif [[ "$line" = "$prompt"* ]]; then + next_state=TEST + # check if start of test + $preserve_prompt && temp_prompt="$prompt" && prompt="" + if [ -z "$test_buffer" ]; then + test_buffer="${line#$prompt}" + else + test_buffer+=$'\n'"${line#$prompt}" + fi + $preserve_prompt && prompt="$temp_prompt" + else + next_state=OUTPUT + output_buffer="$line" + fi + ;; + OUTPUT) + if [[ "$line" = "" ]]; then + next_state=TEXT + doc_test_eval_buffers + [ $? == 1 ] && return 1 + elif [[ "$line" = "$prompt"* ]]; then + next_state=TEST + doc_test_eval_buffers + [ $? == 1 ] && return 1 + $preserve_prompt && temp_prompt="$prompt" && prompt="" + if [ -z "$test_buffer" ]; then + test_buffer="${line#$prompt}" + else + test_buffer+=$'\n'"${line#$prompt}" + fi + $preserve_prompt && prompt="$temp_prompt" + else + next_state=OUTPUT + # check if start of output + if [ -z "$output_buffer" ]; then + output_buffer="$line" + else + output_buffer+=$'\n'"$line" + fi + fi + ;; + esac + state=$next_state + done <<< "$doc_string" + # shellcheck disable=SC2154 + [[ "$(tail --lines=1 <<< "$text_buffer")" = "" ]] && + text_buffer="$(head --lines=-1 <<< "$text_buffer" )" + doc_test_eval_buffers +} +doc_test_doc_identifier=__doc__ +doc_test_doc_regex="/__doc__='/,/';$/p" +doc_test_doc_regex_one_line="__doc__='.*';$" +doc_test_get_function_docstring() { + function="$1" + ( + unset $doc_test_doc_identifier + if ! doc_string="$(type "$function" | \ + grep "$doc_test_doc_regex_one_line")" + then + doc_string="$(type "$function" | sed --quiet "$doc_test_doc_regex")" + fi + eval "$doc_string" + echo "${!doc_test_doc_identifier}" + ) +} +doc_test_print_declaration_warning() { + local module="$1" + local function="$2" + local test_name="$module" + [[ -z "$function" ]] || test_name="$function" + [[ "$doc_test_declarations_diff" == "" ]] && return + core.unique <<< "$doc_test_declarations_diff" \ + | while read -r variable_or_function + do + if ! [[ "$variable_or_function" =~ ^${module}[._]* ]]; then + logging.warn "Test '$test_name' defines unprefixed" \ + "name: '$variable_or_function'" + fi + done +} +doc_test_exceptions_active=false +doc_test_test_module() { + ( + module=$1 + core.import "$module" "$doc_test_supress_declaration" + doc_test_module_under_test="$(core.abs_path "$module")" + declared_functions="$core_declared_functions_after_import" + module="$(basename "$module")" + module="${module%.sh}" + declared_module_functions="$(! declare -F | cut -d' ' -f3 | grep -e "^${module%.sh}" )" + declared_functions="$declared_functions"$'\n'"$declared_module_functions" + declared_functions="$(core.unique <(echo "$declared_functions"))" + + local total=0 + local success=0 + time.timer_start + # module level tests + test_identifier="${module//[^[:alnum:]_]/_}"__doc__ + doc_string="${!test_identifier}" + if ! [ -z "$doc_string" ]; then + let "total++" + doc_test_run_test "$doc_string" "$module" && let "success++" + fi + # function level tests + # TODO detect and warn doc_strings with double quotes + test_identifier=__doc__ + for fun in $declared_functions; do + # shellcheck disable=SC2089 + doc_string="$(doc_test_get_function_docstring "$fun")" + if [[ "$doc_string" != "" ]]; then + let "total++" + doc_test_run_test "$doc_string" "$module" "$fun" && let "success++" + else + ! $doc_test_supress_undocumented && \ + logging.warn "undocumented function $fun" + fi + done + logging.info "$module - passed $success/$total tests in" \ + "$(time.timer_get_elapsed) ms" + (( success != total )) && exit 1 + exit 0 + ) +} +doc_test_parse_args() { + local __doc__=' + +documentation_exclude + >>> doc_test_parse_args non_existing_module + >>> echo $? + +doc_test_contains + +doc_test_ellipsis + Failed to test file: non_existing_module + ... + 1 + + -documentation_exclude + ' + local filename module directory verbose help + arguments.set "$@" + arguments.get_flag --help -h help + $help && documentation.print_doc_string "$doc_test__doc__" && return 0 + arguments.get_flag --side-by-side doc_test_use_side_by_side_output + # do not warn about unprefixed names + arguments.get_flag --no-check-namespace doc_test_supress_declaration + # do not warn about undocumented functions + arguments.get_flag --no-check-undocumented doc_test_supress_undocumented + # use set -o nounset inside tests + arguments.get_flag --use-nounset doc_test_nounset + arguments.get_flag --verbose -v verbose + arguments.apply_new_arguments + + if $verbose; then + logging.set_level verbose + else + logging.set_level info + fi + doc_test_test_directory() { + directory="$(core.abs_path "$1")" + for filename in "$directory"/*.sh; do + let "total++" + doc_test_test_module "$(core.abs_path "$filename")" & + done + } + time.timer_start + local total=0 + local success=0 + if [ $# -eq 0 ] || [ "$@" == "" ];then + doc_test_test_directory "$(dirname "$0")" + else + for filename in "$@"; do + if [ -f "$filename" ]; then + let "total++" + doc_test_test_module "$(core.abs_path "$filename")" & + elif [ -d "$filename" ]; then + doc_test_test_directory "$filename" + else + let "total++" + logging.warn "Failed to test file: $filename" + fi + done + fi + local job + for job in $(jobs -p); do + wait "$job" && let "success++" + done + logging.info "Total: passed $success/$total modules in" \ + "$(time.timer_get_elapsed) ms" + (( success != total )) && return 1 + return 0 +} + +if core.is_main; then + doc_test_parse_args "$@" +fi +# region vim modline + +# vim: set tabstop=4 shiftwidth=4 expandtab: +# vim: foldmethod=marker foldmarker=region,endregion: + +# endregion diff --git a/src/documentation.sh b/src/documentation.sh new file mode 100755 index 0000000..137f2ab --- /dev/null +++ b/src/documentation.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source "$(dirname "$(readlink --canonicalize "${BASH_SOURCE[0]}")")/core.sh" + +core.import doc_test +core.import logging +core.import utils +core.import arguments +documentation_format_buffers() { + local buffer="$1" + local output_buffer="$2" + local text_buffer="$3" + [[ "$text_buffer" != "" ]] && echo "$text_buffer" + if [[ "$buffer" != "" ]]; then + # shellcheck disable=SC2016 + echo '```bash' + echo "$buffer" + echo "$output_buffer" + # shellcheck disable=SC2016 + echo '```' + fi +} +documentation_format_docstring() { + local doc_string="$1" + doc_string="$(echo "$doc_string" \ + | sed '/+documentation_exclude_print/d' \ + | sed '/-documentation_exclude_print/d' \ + | sed '/+documentation_exclude/,/-documentation_exclude/d')" + doc_test_parse_doc_string "$doc_string" documentation_format_buffers \ + --preserve-prompt +} +documentation_generate() { + # TODO add doc test setup function to documentation + module=$1 + ( + core.import "$module" || logging.warn "Failed to import module $module" + declared_functions="$core_declared_functions_after_import" + module="$(basename "$module")" + module="${module%.sh}" + declared_module_functions="$(! declare -F | cut -d' ' -f3 | grep -e "^${module%.sh}" )" + declared_functions="$declared_functions"$'\n'"$declared_module_functions" + declared_functions="$(core.unique <(echo "$declared_functions"))" + + # module level doc + test_identifier="$module"__doc__ + local doc_string="${!test_identifier}" + logging.plain "## Module $module" + if [[ -z "$doc_string" ]]; then + logging.warn "No top level documentation for module $module" 1>&2 + else + logging.plain "$(documentation_format_docstring "$doc_string")" + fi + + # function level documentation + test_identifier=__doc__ + local function + for function in $declared_functions; + do + # shellcheck disable=SC2089 + doc_string="$(doc_test_get_function_docstring "$function")" + if [[ -z "$doc_string" ]]; then + logging.warn "No documentation for function $function" 1>&2 + else + logging.plain "### Function $function" + logging.plain "$(documentation_format_docstring "$doc_string")" + fi + done + ) +} +documentation_serve() { + local __doc__=' + Serves a readme via webserver. Uses Flatdoc. + ' + local readme="$1" + [[ "$readme" == "" ]] && readme="README.md" + local server_root="$(mktemp --directory)" + cp "$readme" "$server_root/README.md" + pushd "$server_root" + wget --output-document index.html \ + https://cdn.rawgit.com/jandob/rebash/gh-pages/index-local.html + python2 -m SimpleHTTPServer 8080 + popd + rm -rf "$server_root" +} +documentation_parse_args() { + local filename module main_documentation serve + arguments.set "$@" + arguments.get_flag --serve serve + arguments.apply_new_arguments + $serve && documentation_serve "$1" && return 0 + main_documentation="$(dirname "${BASH_SOURCE[0]}")/rebash.md" + if [ $# -eq 0 ]; then + [[ -e "$main_documentation" ]] && cat "$main_documentation" + logging.plain "" + logging.plain "# Generated documentation" + for filename in $(dirname "$0")/*.sh; do + module=$(basename "${filename%.sh}") + documentation_generate "$module" + done + else + logging.plain "# Generated documentation" + for module in "$@"; do + documentation_generate "$(core_abs_path "$module")" + done + fi + return 0 +} +documentation_print_doc_string() { + local doc_string="$1" + echo "$doc_string" \ + | sed '/+documentation_exclude_print/,/-documentation_exclude_print/d' \ + | sed '/+documentation_exclude/,/-documentation_exclude/d' \ + | sed '/```/d' +} +alias documentation.print_doc_string="documentation_print_doc_string" + +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + logging.set_level debug + logging.set_commands_level info + documentation_parse_args "$@" +fi diff --git a/src/exceptions.sh b/src/exceptions.sh new file mode 100644 index 0000000..04342af --- /dev/null +++ b/src/exceptions.sh @@ -0,0 +1,329 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source "$(dirname "${BASH_SOURCE[0]}")/core.sh" +core.import logging + +# shellcheck disable=SC2034,SC2016 +exceptions__doc__=' + NOTE: The try block is executed in a subshell, so no outer variables can be + assigned. + + >>> exceptions.activate + >>> false + +doc_test_ellipsis + Traceback (most recent call first): + ... + + >>> exceptions_activate + >>> exceptions.try { + >>> false + >>> }; exceptions.catch { + >>> echo caught + >>> } + caught + + Exceptions in a subshell: + >>> exceptions_activate + >>> ( false ) + +doc_test_ellipsis + Traceback (most recent call first): + ... + Traceback (most recent call first): + ... + >>> exceptions_activate + >>> exceptions.try { + >>> (false; echo "this should not be printed") + >>> echo "this should not be printed" + >>> }; exceptions.catch { + >>> echo caught + >>> } + +doc_test_ellipsis + caught + + Nested exceptions: + >>> exceptions_foo() { + >>> true + >>> exceptions.try { + >>> false + >>> }; exceptions.catch { + >>> echo caught inside foo + >>> } + >>> false # this is cought at top level + >>> echo this should never be printed + >>> } + >>> + >>> exceptions.try { + >>> exceptions_foo + >>> }; exceptions.catch { + >>> echo caught + >>> } + >>> + caught inside foo + caught + + Exceptions are implicitely active inside try blocks: + >>> foo() { + >>> echo $1 + >>> true + >>> exceptions.try { + >>> false + >>> }; exceptions.catch { + >>> echo caught inside foo + >>> } + >>> false # this is not caught + >>> echo this should never be printed + >>> } + >>> + >>> foo "EXCEPTIONS NOT ACTIVE:" + >>> exceptions_activate + >>> foo "EXCEPTIONS ACTIVE:" + +doc_test_ellipsis + EXCEPTIONS NOT ACTIVE: + caught inside foo + this should never be printed + EXCEPTIONS ACTIVE: + caught inside foo + Traceback (most recent call first): + ... + + Exceptions inside conditionals: + >>> exceptions_activate + >>> false && echo "should not be printed" + >>> (false) && echo "should not be printed" + >>> exceptions.try { + >>> ( + >>> false + >>> echo "should not be printed" + >>> ) + >>> }; exceptions.catch { + >>> echo caught + >>> } + caught + + Print a caught exception traceback. + >>> exceptions.try { + >>> false + >>> }; exceptions.catch { + >>> echo caught + >>> echo "$exceptions_last_traceback" + >>> } + +doc_test_ellipsis + caught + Traceback (most recent call first): + ... + + Different syntax variations are possible. + >>> exceptions.try { + >>> ! true + >>> }; exceptions.catch { + >>> echo caught + >>> } + + >>> exceptions.try + >>> false + >>> exceptions.catch { + >>> echo caught + >>> } + caught + + >>> exceptions.try + >>> false + >>> exceptions.catch + >>> echo caught + caught + + >>> exceptions.try { + >>> false + >>> } + >>> exceptions.catch { + >>> echo caught + >>> } + caught + + >>> exceptions.try { + >>> false + >>> } + >>> exceptions.catch + >>> { + >>> echo caught + >>> } + caught + + Exceptions stay enabled after catch block. + >>> exceptions.activate + >>> exceptions.try { + >>> false + >>> }; exceptions.catch { + >>> echo caught + >>> } + >>> false + >>> echo "should not be printed" + caught + +doc_test_ellipsis + Traceback (most recent call first): + ... +' + +exceptions_active=false +exceptions_active_before_try=false +declare -ig exceptions_try_catch_level=0 + +exceptions_error_handler() { + local error_code=$? + local traceback="Traceback (most recent call first):" + local -i i=0 + while caller $i > /dev/null + do + local -a trace=( $(caller $i) ) + local line=${trace[0]} + local subroutine=${trace[1]} + local filename=${trace[2]} + traceback="$traceback"'\n'"[$i] ${filename}:${line}: ${subroutine}" + ((i++)) + done + if (( exceptions_try_catch_level == 0 )); then + logging_plain "$traceback" 1>&2 + else + logging_plain "$traceback" >"$exceptions_last_traceback_file" + fi + exit $error_code +} +exceptions_deactivate() { + # shellcheck disable=SC2016,2034 + local __doc__=' + >>> set -o errtrace + >>> trap '\''echo $foo'\'' ERR + >>> exceptions.activate + >>> trap -p ERR | cut --delimiter "'\''" --fields 2 + >>> exceptions.deactivate + >>> trap -p ERR | cut --delimiter "'\''" --fields 2 + exceptions_error_handler + echo $foo + ' + $exceptions_active || return 0 + [ "$exceptions_errtrace_saved" = "off" ] && set +o errtrace + [ "$exceptions_pipefail_saved" = "off" ] && set +o pipefail + [ "$exceptions_functrace_saved" = "off" ] && set +o functrace + export PS4="$exceptions_ps4_saved" + # shellcheck disable=SC2064 + trap "$exceptions_err_traps" ERR + exceptions_active=false +} + +exceptions_test_context() { + # shellcheck disable=SC2016,2034 + local __doc__=' + Note: set -e and ERR traps are prevented from working in a subshell if + it is disabled by the surrounding context. + >>> exceptions.activate + >>> exceptions_foo() { + >>> exceptions.try { + >>> false + >>> }; exceptions.catch { + >>> # this is not caught because of the || + >>> echo caught + >>> } + >>> false + >>> echo this should be printed + >>> } + >>> + >>> exceptions_foo || echo "error in exceptions_foo" + >>> + Warning: Context does not allow error trap! + this should be printed + ' + # test if context allows error traps + ( + local exceptions_test_context_pass=1 + set -o errtrace + trap 'exceptions_test_context_pass=0' ERR + false + [ $exceptions_test_context_pass == 1 ] && exit 1 + exit 0 + ) + return $? +} + +exceptions_activate() { + local do_not_check="$1" + if [ -z "$do_not_check" ]; then + exceptions_test_context + [ $? == 1 ] && logging_plain \ + "Warning: Context does not allow error trap!" 2>&1 + fi + $exceptions_active && return 0 + + exceptions_errtrace_saved=$(set -o | awk '/errtrace/ {print $2}') + exceptions_pipefail_saved=$(set -o | awk '/pipefail/ {print $2}') + exceptions_functrace_saved=$(set -o | awk '/functrace/ {print $2}') + exceptions_err_traps=$(trap -p ERR | cut --delimiter "'" --fields 2) + exceptions_ps4_saved="$PS4" + + # improve xtrace output (set -x) + export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' + + # If set, any trap on ERR is inherited by shell functions, + # command substitutions, and commands executed in a subshell environment. + # The ERR trap is normally not inherited in such cases. + set -o errtrace + # If set, any trap on DEBUG and RETURN are inherited by shell functions, + # command substitutions, and commands executed in a subshell environment. + # The DEBUG and RETURN traps are normally not inherited in such cases. + #set -o functrace + # If set, the return value of a pipeline is the value of the last + # (rightmost) command to exit with a non-zero status, or zero if all + # commands in the pipeline exit successfully. This option is disabled by + # default. + set -o pipefail + # Treat unset variables and parameters other than the special parameters + # ‘@’ or ‘*’ as an error when performing parameter expansion. + # An error message will be written to the standard error, and a + # non-interactive shell will exit. + #set -o nounset + + # traps: + # EXIT executed on shell exit + # DEBUG executed before every simple command + # RETURN executed when a shell function or a sourced code finishes executing + # ERR executed each time a command's failure would cause the shell to exit when the '-e' option ('errexit') is enabled + + # ERR is not executed in following cases: + # >>> err() { return 1;} + # >>> ! err + # >>> err || echo foo + # >>> err && echo foo + + trap exceptions_error_handler ERR + #trap exceptions_debug_handler DEBUG + #trap exceptions_exit_handler EXIT + exceptions_active=true +} + +exceptions_enter_try() { + if (( exceptions_try_catch_level == 0 )); then + exceptions_last_traceback_file="$(mktemp --suffix=rebash-exceptions)" + exceptions_active_before_try=$exceptions_active + fi + exceptions_deactivate + exceptions_try_catch_level+=1 +} + +exceptions_exit_try() { + local exceptions_result=$1 + exceptions_try_catch_level+=-1 + if (( exceptions_try_catch_level == 0 )); then + $exceptions_active_before_try && exceptions_activate 1 + exceptions_last_traceback="$( + logging.cat "$exceptions_last_traceback_file" + )" + rm "$exceptions_last_traceback_file" + else + exceptions_activate 1 + fi + return $exceptions_result +} + +alias exceptions.activate="exceptions_activate" +alias exceptions.deactivate="exceptions_deactivate" +alias exceptions.try='exceptions_enter_try; (exceptions_activate; ' +alias exceptions.catch='true); exceptions_exit_try $? || ' diff --git a/src/logging.sh b/src/logging.sh new file mode 100644 index 0000000..9badd6b --- /dev/null +++ b/src/logging.sh @@ -0,0 +1,481 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source "$(dirname "${BASH_SOURCE[0]}")/core.sh" + +core.import ui +core.import array +core.import arguments +logging__doc__=' + The available log levels are: + error critical warn info debug + + The standard loglevel is critical + >>> logging.get_level + >>> logging.get_commands_level + critical + critical + >>> logging.error error-message + >>> logging.critical critical-message + >>> logging.warn warn-message + >>> logging.info info-message + >>> logging.debug debug-message + +doc_test_contains + error-message + critical-message + + If the output of commands should be printed, the commands_level needs to be + greater than or equal to the log_level. + >>> logging.set_level critical + >>> logging.set_commands_level debug + >>> echo foo + + >>> logging.set_level info + >>> logging.set_commands_level info + >>> echo foo + foo + + Another logging prefix can be set by overriding "logging_get_prefix". + >>> logging_get_prefix() { + >>> local level=$1 + >>> echo "[myprefix - ${level}]" + >>> } + >>> logging.critical foo + [myprefix - critical] foo + + "logging.plain" can be used to print at any log level and without the + prefix. + >>> logging.set_level critical + >>> logging.set_commands_level debug + >>> logging.plain foo + foo + + "logging.cat" can be used to print files (e.g "logging.cat < file.txt") + or heredocs. Like "logging.plain", it also prints at any log level and + without the prefix. + >>> echo foo | logging.cat + foo +' + +# region variables +# logging levels from low to high +logging_levels=(error critical warn info verbose debug) +# matches the order of logging levels +logging_levels_color=( + $ui_color_red + $ui_color_magenta + $ui_color_yellow + $ui_color_cyan + $ui_color_green + $ui_color_blue +) +logging_commands_level=$(array.get_index 'critical' "${logging_levels[@]}") +logging_level=$(array.get_index 'critical' "${logging_levels[@]}") +# endregion +# region functions +logging_set_commands_level() { + logging_commands_level=$(array.get_index "$1" "${logging_levels[@]}") + if [ "$logging_level" -ge "$logging_commands_level" ]; then + logging_set_command_output_on + else + logging_set_command_output_off + fi +} +logging_get_level() { + echo "${logging_levels[$logging_level]}" +} +logging_get_commands_level() { + echo "${logging_levels[$logging_commands_level]}" +} +logging_set_level() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + >>> logging.set_commands_level info + >>> logging.set_level info + >>> echo $logging_level + >>> echo $logging_commands_level + 3 + 3 + ' + logging_level=$(array.get_index "$1" "${logging_levels[@]}") + if [ "$logging_level" -ge "$logging_commands_level" ]; then + logging_set_command_output_on + else + logging_set_command_output_off + fi +} +logging_get_prefix() { + local level=$1 + local level_index=$2 + local color=${logging_levels_color[$level_index]} + # shellcheck disable=SC2154 + local loglevel=${color}${level}${ui_color_default} + local path="${BASH_SOURCE[2]##./}" + path=$(basename "$path") + local prefix=[${loglevel}:"$path":${BASH_LINENO[1]}] + echo "$prefix" +} +logging_log() { + local level="$1" + shift + local level_index + level_index=$(array.get_index "$level" "${logging_levels[@]}") + if [ "$level_index" -eq -1 ]; then + logging_log critical "loglevel \"$level\" not available, use one of: "\ + "${logging_levels[@]}" + return 1 + fi + if [ "$logging_level" -ge "$level_index" ]; then + logging_plain "$(logging_get_prefix "$level" "$level_index")" "$@" + fi +} +logging_output_to_saved_file_descriptors=false +logging_off=false +logging_cat() { + $logging_off && return 0 + if [[ "$logging_log_file" != "" ]]; then + cat "$@" >> "$logging_log_file" + if $logging_tee_fifo_active; then + cat "$@" + fi + else + if $logging_output_to_saved_file_descriptors; then + cat "$@" 1>&3 2>&4 + else + cat "$@" + fi + fi +} +logging_plain() { + local __doc__=' + >>> logging.set_level info + >>> logging.set_commands_level debug + >>> logging.debug "not shown" + >>> echo "not shown" + >>> logging.plain "shown" + shown + + ' + $logging_off && return 0 + if [[ "$logging_log_file" != "" ]]; then + echo -e "$@" >> "$logging_log_file" + if $logging_tee_fifo_active; then + echo -e "$@" + fi + else + if $logging_output_to_saved_file_descriptors; then + echo -e "$@" 1>&3 2>&4 + else + echo -e "$@" + fi + fi +} +logging_commands_output_saved="std" +logging_set_command_output_off() { + logging_commands_output_saved="$logging_options_command" + logging_set_file_descriptors "$logging_log_file" \ + --logging="$logging_options_log" --commands="off" +} +logging_set_command_output_on() { + logging_set_file_descriptors "$logging_log_file" \ + --logging="$logging_options_log" \ + --commands="std" +} +logging_log_file='' +# shellcheck disable=SC2034 +logging_tee_fifo="" +logging_tee_fifo_dir="" +logging_tee_fifo_active=false +logging_file_descriptors_saved=false +logging_commands_tee_fifo_active=false +logging_options_log="std" +logging_options_command="std" +logging_set_log_file() { + local __doc__=' + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging.set_log_file "$test_file" + >>> logging.plain logging + >>> logging.set_log_file "$test_file" + >>> echo echo + >>> logging.set_log_file "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + logging + echo + test_file: + logging + echo + + >>> logging.set_commands_level debug + >>> logging.set_level debug + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging.set_log_file "$test_file" + >>> logging.plain 1 + >>> logging.set_log_file "" + >>> logging.set_log_file "$test_file" + >>> logging.plain 2 + >>> logging.set_log_file "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + 1 + 2 + test_file: + 1 + 2 + ' + [[ "$logging_log_file" == "$1" ]] && return 0 + logging_set_file_descriptors "" + [[ "$1" == "" ]] && return 0 + logging_set_file_descriptors "$1" --commands=tee --logging=tee +} +logging_set_file_descriptors() { + local __doc__=' + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + test_file: + + >>> local test_file="$(mktemp)" + >>> logging_set_file_descriptors "$test_file" + >>> logging_set_file_descriptors "" + >>> echo "test_file:" >"$test_file" + >>> logging.cat "$test_file" + >>> rm "$test_file" + test_file: + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=tee + >>> logging.plain foo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + foo + test_file: + foo + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=off --commands=file + >>> logging.plain not shown + >>> echo foo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + test_file: + foo + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=off + >>> logging.plain not shown + >>> echo foo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + foo + test_file: + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --commands=tee + >>> logging.plain logging + >>> echo echo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + logging + echo + test_file: + echo + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --commands=file + >>> logging.plain logging + >>> echo echo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + logging + test_file: + echo + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=file --commands=file + >>> logging.plain logging + >>> echo echo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + test_file: + logging + echo + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=file --commands=file + >>> logging.plain logging + >>> echo echo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + test_file: + logging + echo + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=file --commands=tee + >>> logging.plain logging + >>> echo echo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + echo + test_file: + logging + echo + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=file --commands=off + >>> logging.plain logging + >>> echo echo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + test_file: + logging + + >>> local test_file="$(mktemp)" + >>> logging.plain "test_file:" >"$test_file" + >>> logging_set_file_descriptors "$test_file" --logging=tee --commands=tee + >>> logging.plain logging + >>> echo echo + >>> logging_set_file_descriptors "" + >>> logging.cat "$test_file" + >>> rm "$test_file" + logging + echo + test_file: + logging + echo + + Test exit handler + >>> local test_file fifo + >>> test_file="$(mktemp)" + >>> fifo=$(logging_set_file_descriptors "$test_file" --commands=tee; \ + >>> echo $logging_tee_fifo) + >>> [ -p "$fifo" ] || echo fifo deleted + >>> rm "$test_file" + fifo deleted + ' + arguments.set "$@" + # one off "std off tee file" + local options_log options_command + arguments.get_keyword --logging options_log + arguments.get_keyword --commands options_command + [[ "${options_log-}" == "" ]] && options_log=std + [[ "${options_command-}" == "" ]] && options_command=std + logging_options_log="$options_log" + logging_options_command="$options_command" + set -- "${arguments_new_arguments[@]:-}" + local log_file="$1" + + logging_off=false + # restore + if $logging_file_descriptors_saved; then + exec 1>&3 2>&4 3>&- 4>&- + logging_file_descriptors_saved=false + fi + [ -p "$logging_tee_fifo" ] && rm -rf "$logging_tee_fifo_dir" + logging_commands_tee_fifo_active=false + logging_tee_fifo_active=false + logging_output_to_saved_file_descriptors=false + + if [[ "$log_file" == "" ]]; then + logging_log_file="" + [[ "$logging_options_log" == "tee" ]] && return 1 + [[ "$logging_options_command" == "tee" ]] && return 1 + if [[ "$logging_options_log" == "off" ]]; then + logging_off=true + fi + if [[ "$logging_options_command" == "off" ]]; then + exec 3>&1 4>&2 + logging_file_descriptors_saved=true + exec &>/dev/null + logging_output_to_saved_file_descriptors=true + fi + return 0 + fi + # It's guaranteed that we have a log file from here on. + + if ! $logging_file_descriptors_saved; then + # save /dev/stdout and /dev/stderr to &3, &4 + exec 3>&1 4>&2 + logging_file_descriptors_saved=true + fi + + if [[ "$logging_options_log" == tee ]]; then + if [[ "$logging_options_command" != "tee" ]]; then + logging_log_file="$log_file" + logging_tee_fifo_active=true + fi + elif [[ "$logging_options_log" == "stdout" ]]; then + true + elif [[ "$logging_options_log" == "file" ]]; then + logging_log_file="$log_file" + elif [[ "$logging_options_log" == "off" ]]; then + logging_off=true + fi + if [[ "$logging_options_command" == "tee" ]]; then + logging_tee_fifo_dir="$(mktemp --directory --suffix rebash-logging-fifo)" + logging_tee_fifo="$logging_tee_fifo_dir/fifo" + mkfifo "$logging_tee_fifo" + trap '[ -p "$logging_tee_fifo" ] && rm -rf "$logging_tee_fifo_dir"; exit' EXIT + tee --append "$log_file" <"$logging_tee_fifo" & + exec 1>>"$logging_tee_fifo" 2>>"$logging_tee_fifo" + logging_commands_tee_fifo_active=true + if [[ "$logging_options_log" != tee ]]; then + logging_output_to_saved_file_descriptors=true + fi + elif [[ "$logging_options_command" == "stdout" ]]; then + true + elif [[ "$logging_options_command" == "file" ]]; then + exec 1>>"$log_file" 2>>"$log_file" + logging_output_to_saved_file_descriptors=true + elif [[ "$logging_options_command" == "off" ]]; then + exec 1>>/dev/null 2>>/dev/null + fi +} + +# endregion +# region public interface +alias logging.set_level='logging_set_level' +alias logging.set_commands_level='logging_set_commands_level' +alias logging.get_level='logging_get_level' +alias logging.get_commands_level='logging_get_commands_level' +alias logging.log='logging_log' +alias logging.error='logging_log error' +alias logging.critical='logging_log critical' +alias logging.warn='logging_log warn' +alias logging.info='logging_log info' +alias logging.verbose='logging_log verbose' +alias logging.debug='logging_log debug' +alias logging.plain='logging_plain' +alias logging.cat='logging_cat' +alias logging.set_file_descriptors='logging_set_file_descriptors' +alias logging.set_log_file='logging_set_log_file' +# endregion +# region vim modline + +# vim: set tabstop=4 shiftwidth=4 expandtab: +# vim: foldmethod=marker foldmarker=region,endregion: + +# endregion diff --git a/src/time.sh b/src/time.sh new file mode 100644 index 0000000..a370074 --- /dev/null +++ b/src/time.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source $(dirname ${BASH_SOURCE[0]})/core.sh + +time_timer_start_time="" +time_timer_start() { + time_timer_start_time=$(date +%s%N) +} +time_timer_get_elapsed() { + local end_time="$(date +%s%N)" + local elapsed_time_in_ns=$(( $end_time - $time_timer_start_time )) + local elapsed_time_in_ms=$(( $elapsed_time_in_ns / 1000000 )) + echo "$elapsed_time_in_ms" +} +alias time.timer_start="time_timer_start" +alias time.timer_get_elapsed="time_timer_get_elapsed" diff --git a/src/ui.sh b/src/ui.sh new file mode 100644 index 0000000..6fa69a8 --- /dev/null +++ b/src/ui.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source $(dirname ${BASH_SOURCE[0]})/core.sh +# shellcheck disable=SC2034 +ui__doc__=' + This module provides variables for printing colorful and unicode glyphs. + The Terminal features are detected automatically but can also be + enabled/disabled manually (see + [ui.enable_color](#function-ui_enable_color) and + [ui.enable_unicode_glyphs](#function-ui_enable_unicode_glyphs)). +' +# region colors +ui_color_enabled=false +ui_enable_color() { + local __doc__=' + Enables color output explicitly. + + >>> ui.disable_color + >>> ui.enable_color + >>> echo -E $ui_color_red red $ui_color_default + \033[0;31m red \033[0m + ' + ui_color_enabled=true + ui_color_default='\033[0m' + + ui_color_black='\033[0;30m' + ui_color_red='\033[0;31m' + ui_color_green='\033[0;32m' + ui_color_yellow='\033[0;33m' + ui_color_blue='\033[0;34m' + ui_color_magenta='\033[0;35m' + ui_color_cyan='\033[0;36m' + ui_color_lightgray='\033[0;37m' + + ui_color_darkgray='\033[0;90m' + ui_color_lightred='\033[0;91m' + ui_color_lightgreen='\033[0;92m' + ui_color_lightyellow='\033[0;93m' + ui_color_lightblue='\033[0;94m' + ui_color_lightmagenta='\033[0;95m' + ui_color_lightcyan='\033[0;96m' + ui_color_white='\033[0;97m' + + # flags + ui_color_bold='\033[1m' + ui_color_dim='\033[2m' + ui_color_underline='\033[4m' + ui_color_blink='\033[5m' + ui_color_invert='\033[7m' + ui_color_invisible='\033[8m' + + ui_color_nobold='\033[21m' + ui_color_nodim='\033[22m' + ui_color_nounderline='\033[24m' + ui_color_noblink='\033[25m' + ui_color_noinvert='\033[27m' + ui_color_noinvisible='\033[28m' +} + +# shellcheck disable=SC2034 +ui_disable_color() { + local __doc__=' + Disables color output explicitly. + + >>> ui.enable_color + >>> ui.disable_color + >>> echo -E "$ui_color_red" red "$ui_color_default" + red + ' + ui_color_enabled=false + ui_color_default='' + + ui_color_black='' + ui_color_red='' + ui_color_green='' + ui_color_yellow='' + ui_color_blue='' + ui_color_magenta='' + ui_color_cyan='' + ui_color_lightgray='' + + ui_color_darkgray='' + ui_color_lightred='' + ui_color_lightgreen='' + ui_color_lightyellow='' + ui_color_lightblue='' + ui_color_lightmagenta='' + ui_color_lightcyan='' + ui_color_white='' + + # flags + ui_color_bold='' + ui_color_dim='' + ui_color_underline='' + ui_color_blink='' + ui_color_invert='' + ui_color_invisible='' + + ui_color_nobold='' + ui_color_nodim='' + ui_color_nounderline='' + ui_color_noblink='' + ui_color_noinvert='' + ui_color_noinvisible='' +} +# endregion +# region glyphs +# NOTE: use 'xfd -fa ' to watch glyphs +ui_unicode_enabled=false +ui_enable_unicode_glyphs() { + local __doc__=' + Enables unicode glyphs explicitly. + + >>> ui.disable_unicode_glyphs + >>> ui.enable_unicode_glyphs + >>> echo -E "$ui_powerline_ok" + \u2714 + ' + ui_unicode_enabled=true + ui_powerline_pointingarrow='\u27a1' + ui_powerline_arrowleft='\ue0b2' + ui_powerline_arrowright='\ue0b0' + ui_powerline_arrowrightdown='\u2198' + ui_powerline_arrowdown='\u2b07' + ui_powerline_plusminus='\ue00b1' + ui_powerline_branch='\ue0a0' + ui_powerline_refersto='\u27a6' + ui_powerline_ok='\u2714' + ui_powerline_fail='\u2718' + ui_powerline_lightning='\u26a1' + ui_powerline_cog='\u2699' + ui_powerline_heart='\u2764' + + # colorful + ui_powerline_star='\u2b50' + ui_powerline_saxophone='\u1f3b7' + ui_powerline_thumbsup='\u1f44d' +} + +# shellcheck disable=SC2034 +ui_disable_unicode_glyphs() { + local __doc__=' + Disables unicode glyphs explicitly. + + >>> ui.enable_unicode_glyphs + >>> ui.disable_unicode_glyphs + >>> echo -E "$ui_powerline_ok" + + + ' + ui_unicode_enabled=false + ui_powerline_pointingarrow='~' + ui_powerline_arrowleft='<' + ui_powerline_arrowright='>' + ui_powerline_arrowrightdown='>' + ui_powerline_arrowdown='_' + ui_powerline_plusminus='+-' + ui_powerline_branch='|}' + ui_powerline_refersto='*' + ui_powerline_ok='+' + ui_powerline_fail='x' + ui_powerline_lightning='!' + ui_powerline_cog='{*}' + ui_powerline_heart='<3' + + # colorful + ui_powerline_star='*' + ui_powerline_saxophone='(yeah)' + ui_powerline_thumbsup='(ok)' +} +# endregion +# region detect terminal capabilities +if [[ "${TERM}" == *"xterm"* ]]; then + ui_enable_color +else + ui_disable_color +fi + +# TODO improve unicode detection +ui_glyph_available_in_font() { + + #local font=$1 + local current_font + current_font=$(xrdb -q| grep -i facename | cut -d: -f2) + local font_file_name + font_file_name=$(fc-match "$current_font" | cut -d: -f1) + #font_path=$(fc-list "$current_font" | grep "$font_file_name" | cut -d: -f1) + local font_file_extension="${font_file_name##*.}" + + # Alternative or to be sure + #font_path=$(lsof -p $(ps -o ppid= -p $$) | grep fonts) + + if [[ $font_file_extension == otf ]]; then + otfinfo /usr/share/fonts/OTF/Hack-Regular.otf -u | grep -i uni27a1 + elif [[ $font_file_extension == ttf ]]; then + ttfdump -t cmap /usr/share/fonts/TTF/Hack-Regular.ttf 2>/dev/null| grep 'Char 0x27a1' + else + return 1 + fi + return $? +} +# TODO this breaks dracut (segfault) +#(echo -e $'\u1F3B7' | grep -v F3B7) &> /dev/null +if core_is_defined NO_UNICODE; then + ui_disable_unicode_glyphs +else + ui_enable_unicode_glyphs +fi +# endregion +# region public interface +alias ui.enable_color='ui_enable_color' +alias ui.disable_color='ui_disable_color' +alias ui.enable_unicode_glyphs='ui_enable_unicode_glyphs' +alias ui.disable_unicode_glyphs='ui_disable_unicode_glyphs' +# endregion +# region vim modline + +# vim: set tabstop=4 shiftwidth=4 expandtab: +# vim: foldmethod=marker foldmarker=region,endregion: + +# endregion diff --git a/src/utils.sh b/src/utils.sh new file mode 100644 index 0000000..cb116f2 --- /dev/null +++ b/src/utils.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# shellcheck source=./core.sh +source "$(dirname "${BASH_SOURCE[0]}")/core.sh" +core.import logging + +utils_dependency_check_pkgconfig() { + local __doc__=' + This function check if all given libraries can be found. + + #### Example: + + >>> utils_dependency_check_shared_library libc.so; echo $? + 0 + >>> utils_dependency_check_shared_library libc.so __not_existing__ 1>/dev/null; echo $? + 2 + >>> utils_dependency_check_shared_library __not_existing__ 1>/dev/null; echo $? + 2 + ' + local return_code=0 + local library + + if ! utils_dependency_check pkg-config &>/dev/null; then + logging.critical 'Missing dependency "pkg-config" to check for packages.' + return 1 + fi + for library in $@; do + if ! pkg-config "$library" &>/dev/null; then + return_code=2 + echo "$library" + fi + done + return $return_code +} +utils_dependency_check_shared_library() { + local __doc__=' + This function check if all given shared libraries can be found. + + #### Example: + + >>> utils_dependency_check_shared_library libc.so; echo $? + 0 + >>> utils_dependency_check_shared_library libc.so __not_existing__ 1>/dev/null; echo $? + 2 + >>> utils_dependency_check_shared_library __not_existing__ 1>/dev/null; echo $? + 2 + ' + local return_code=0 + local pattern + + if ! utils_dependency_check ldconfig &>/dev/null; then + logging.critical 'Missing dependency "ldconfig" to check for shared libraries.' + return 1 + fi + for pattern in $@; do + if ! ldconfig --print-cache | cut --fields 1 --delimiter ' ' | \ + grep "$pattern" &>/dev/null + then + return_code=2 + echo "$pattern" + fi + done + return $return_code +} +utils_dependency_check() { + # shellcheck disable=SC2034 + local __doc__=' + This function check if all given dependencies are present. + + #### Example: + + >>> utils_dependency_check mkdir ls; echo $? + 0 + >>> utils_dependency_check mkdir __not_existing__ 1>/dev/null; echo $? + 2 + >>> utils_dependency_check __not_existing__ 1>/dev/null; echo $? + 2 + >>> utils_dependency_check "ls __not_existing__"; echo $? + __not_existing__ + 2 + ' + local return_code=0 + local dependency + + if ! hash &>/dev/null; then + logging.critical 'Missing dependency "hash" to check for available executables.' + return 1 + fi + for dependency in $@; do + if ! hash "$dependency" &>/dev/null; then + return_code=2 + echo "$dependency" + fi + done + return $return_code +} +utils__doc_test_setup__=' +lsblk() { + if [[ "${@: -1}" == "" ]];then + echo "lsblk: : not a block device" + return 1 + fi + if [[ "${@: -1}" != "/dev/sdb" ]];then + echo "/dev/sda disk" + echo "/dev/sda1 part SYSTEM_LABEL 0x7" + echo "/dev/sda2 part" + fi + if [[ "${@: -1}" != "/dev/sda" ]];then + echo "/dev/sdb disk" + echo "/dev/sdb1 part boot_partition " + echo "/dev/sdb2 part system_partition" + fi +} +blkid() { + [[ "${@: -1}" != "/dev/sda2" ]] && return 0 + echo "gpt" + echo "only discoverable by blkid" + echo "boot_partition" + echo "192d8b9e" +} +' +utils_find_block_device() { + # shellcheck disable=SC2034,SC2016 + local __doc__=' + >>> utils_find_block_device "boot_partition" + /dev/sdb1 + >>> utils_find_block_device "boot_partition" /dev/sda + /dev/sda2 + >>> utils_find_block_device "discoverable by blkid" + /dev/sda2 + >>> utils_find_block_device "_partition" + /dev/sdb1 /dev/sdb2 + >>> utils_find_block_device "not matching anything" || echo not found + not found + >>> utils_find_block_device "" || echo not found + not found + ' + local partition_pattern="$1" + local device="${2-}" + + [ "$partition_pattern" = "" ] && return 1 + utils_find_block_device_simple() { + local device_info + lsblk --noheadings --list --paths --output \ + NAME,TYPE,LABEL,PARTLABEL,UUID,PARTUUID ${device:+"$device"} \ + | sort --unique | while read -r device_info; do + local current_device + current_device=$(echo "$device_info" | cut -d' ' -f1) + if [[ "$device_info" = *"${partition_pattern}"* ]]; then + echo "$current_device" + fi + done + } + utils_find_block_device_deep() { + local device_info + lsblk --noheadings --list --paths --output NAME ${device:+"$device"} \ + | sort --unique | cut -d' ' -f1 | while read -r current_device; do + blkid -p -o value "$current_device" \ + | while read -r device_info; do + if [[ "$device_info" = *"${partition_pattern}"* ]]; then + echo "$current_device" + fi + done + done + } + local candidates + candidates=($(utils_find_block_device_simple)) + [ ${#candidates[@]} -eq 0 ] && candidates=($(utils_find_block_device_deep)) + unset -f utils_find_block_device_simple + unset -f utils_find_block_device_deep + [ ${#candidates[@]} -eq 0 ] && return 1 + [ ${#candidates[@]} -ne 1 ] && echo "${candidates[@]}" && return 1 + logging.plain "${candidates[0]}" +} +utils_create_partition_via_offset() { + local device="$1" + local nameOrUUID="$2" + local loop_device + loop_device="$(losetup --find)" + local sector_size + sector_size="$(blockdev --getbsz "$device")" + + # NOTE: partx's NAME field corresponds to partition labels + local partitionInfo + partitionInfo=$(partx --raw --noheadings --output \ + START,NAME,UUID,TYPE "$device" 2>/dev/null| grep "$nameOrUUID") + local offsetSectors + offsetSectors="$(echo "$partitionInfo"| cut --delimiter ' ' \ + --fields 1)" + if [ -z "$offsetSectors" ]; then + logging.warn "Could not find partition with label/uuid \"$nameOrUUID\" on device \"$device\"" + return 1 + fi + local offsetBytes + offsetBytes="$(echo | awk -v x="$offsetSectors" -v y="$sector_size" '{print x * y}')" + losetup --offset "$offsetBytes" "$loop_device" "$device" + logging.plain "$loop_device" +} +utils_random_string() { + local length="$1" + cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c "$length" +} +alias utils.dependency_check_pkgconfig="utils_dependency_check_pkgconfig" +alias utils.dependency_check_shared_library="utils_dependency_check_shared_library" +alias utils.dependency_check="utils_dependency_check" +alias utils.find_block_device="utils_find_block_device" +alias utils.create_partition_via_offset="utils_create_partition_via_offset" +alias utils.random_string="utils_random_string" diff --git a/test/mockup_module-b.sh b/test/mockup_module-b.sh index 6295d6f..10e44a0 100644 --- a/test/mockup_module-b.sh +++ b/test/mockup_module-b.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # shellcheck source=./core.sh -source "$(dirname "${BASH_SOURCE[0]}")/../core.sh" +source "$(dirname "${BASH_SOURCE[0]}")/../src/core.sh" core.import logging core.import mockup_module_c.sh echo imported module b diff --git a/test/mockup_module_a.sh b/test/mockup_module_a.sh index 2dee914..f24c6a1 100644 --- a/test/mockup_module_a.sh +++ b/test/mockup_module_a.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # shellcheck source=../core.sh -source "$(dirname "${BASH_SOURCE[0]}")/../core.sh" +source "$(dirname "${BASH_SOURCE[0]}")/../src/core.sh" mockup_module_a_foo() { echo "a" } diff --git a/test/mockup_module_c.sh b/test/mockup_module_c.sh index c2b4df7..f92aac3 100644 --- a/test/mockup_module_c.sh +++ b/test/mockup_module_c.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # shellcheck source=./core.sh -source "$(dirname "${BASH_SOURCE[0]}")/../core.sh" +source "$(dirname "${BASH_SOURCE[0]}")/../src/core.sh" core.import logging core.import mockup_module-b.sh foo123() { From ba5e3ef298f0d636f5f0b14f3966fbdb320b2739 Mon Sep 17 00:00:00 2001 From: Devon Bagley Date: Wed, 27 Oct 2021 17:45:43 -0700 Subject: [PATCH 5/7] complete move of files --- arguments.sh | 241 ---------------- array.sh | 166 ----------- btrfs.sh | 357 ------------------------ change_root.sh | 146 ---------- core.sh | 406 --------------------------- dictionary.sh | 143 ---------- doc_test.sh | 707 ----------------------------------------------- documentation.sh | 121 -------- exceptions.sh | 329 ---------------------- logging.sh | 481 -------------------------------- time.sh | 16 -- ui.sh | 220 --------------- utils.sh | 207 -------------- 13 files changed, 3540 deletions(-) delete mode 100644 arguments.sh delete mode 100644 array.sh delete mode 100644 btrfs.sh delete mode 100644 change_root.sh delete mode 100644 core.sh delete mode 100644 dictionary.sh delete mode 100755 doc_test.sh delete mode 100755 documentation.sh delete mode 100644 exceptions.sh delete mode 100644 logging.sh delete mode 100644 time.sh delete mode 100644 ui.sh delete mode 100644 utils.sh diff --git a/arguments.sh b/arguments.sh deleted file mode 100644 index 9172f42..0000000 --- a/arguments.sh +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source $(dirname ${BASH_SOURCE[0]})/core.sh -core.import array -# shellcheck disable=SC2034,SC2016 -arguments__doc__=' - The arguments module provides an argument parser that can be used in - functions and scripts. - - Different functions are provided in order to parse an arguments array. - - #### Example - >>> _() { - >>> local value - >>> arguments.set "$@" - >>> arguments.get_parameter param1 value - >>> echo "param1: $value" - >>> arguments.get_keyword keyword2 value - >>> echo "keyword2: $value" - >>> arguments.get_flag --flag4 value - >>> echo "--flag4: $value" - >>> # NOTE: Get the positionals last - >>> arguments.get_positional 1 value - >>> echo 1: "$value" - >>> # Alternative way to get positionals: Set the arguments array to - >>> # to all unparsed arguments. - >>> arguments.apply_new_arguments - >>> echo 1: "$1" - >>> } - >>> _ param1 value1 keyword2=value2 positional3 --flag4 - param1: value1 - keyword2: value2 - --flag4: true - 1: positional3 - 1: positional3 - -' -arguments_new_arguments=() -arguments_set() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - ``` - arguments.set argument1 argument2 ... - ``` - - Set the array the arguments-module is working on. After getting the desired - arguments, the new argument array can be accessed via - `arguments_new_arguments`. This new array contains all remaining arguments. - - ' - arguments_new_arguments=("$@") - -} -arguments_get_flag() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - ``` - arguments.get_flag flag [flag_aliases...] variable_name - ``` - - Sets `variable_name` to true if flag (or on of its aliases) is contained in - the argument array (see `arguments.set`) - - #### Example - ``` - arguments.get_flag verbose --verbose -v verbose_is_set - ``` - - #### Tests - >>> arguments.set other_param1 --foo other_param2 - >>> local foo bar - >>> arguments.get_flag --foo -f foo - >>> echo $foo - >>> arguments.get_flag --bar bar - >>> echo $bar - >>> echo "${arguments_new_arguments[@]}" - true - false - other_param1 other_param2 - - >>> arguments.set -f - >>> local foo - >>> arguments.get_flag --foo -f foo - >>> echo $foo - true - - ' - local variable match argument flag - local flag_aliases=($(array.slice :-1 "$@")) - variable="$(array.slice -1 "$@")" - local new_arguments=() - eval "${variable}=false" - for argument in "${arguments_new_arguments[@]:-}"; do - match=false - for flag in "${flag_aliases[@]}"; do - if [[ "$argument" == "$flag" ]]; then - match=true - eval "${variable}=true" - fi - done - $match || new_arguments+=( "$argument" ) - done - arguments_new_arguments=( "${new_arguments[@]:+${new_arguments[@]}}" ) -} -arguments_get_keyword() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - ``` - arguments.get_keyword keyword variable_name - ``` - - Sets `variable_name` to the "value" of `keyword` the argument array (see - `arguments.set`) contains "keyword=value". - - #### Example - ``` - arguments.get_keyword log loglevel - ``` - #### Tests - >>> local foo - >>> arguments.set other_param1 foo=bar baz=baz other_param2 - >>> arguments.get_keyword foo foo - >>> echo $foo - >>> echo "${arguments_new_arguments[@]}" - bar - other_param1 baz=baz other_param2 - - >>> local foo - >>> arguments.set other_param1 foo=bar baz=baz other_param2 - >>> arguments.get_keyword foo - >>> echo $foo - >>> arguments.get_keyword baz foo - >>> echo $foo - bar - baz - ' - local keyword="$1" - local variable="$1" - [[ "${2:-}" != "" ]] && variable="$2" - # NOTE: use unique variable name "value_csh94wwn25" here as this prevents - # evaling something like "value=$value" - local argument key value_csh94wwn25 - local new_arguments=() - for argument in "${arguments_new_arguments[@]:-}"; do - if [[ "$argument" == *=* ]]; then - IFS="=" read -r key value_csh94wwn25 <<<"$argument" - if [[ "$key" == "$keyword" ]]; then - eval "${variable}=$value_csh94wwn25" - else - new_arguments+=( "$argument" ) - fi - else - new_arguments+=( "$argument" ) - fi - done - arguments_new_arguments=( "${new_arguments[@]:+${new_arguments[@]}}" ) -} -arguments_get_parameter() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - ``` - arguments.get_parameter parameter [parameter_aliases...] variable_name - ``` - - Sets `variable_name` to the field following `parameter` (or one of the - `parameter_aliases`) from the argument array (see `arguments.set`). - - #### Example - ``` - arguments.get_parameter --log-level -l loglevel - ``` - - #### Tests - >>> local foo - >>> arguments.set other_param1 --foo bar other_param2 - >>> arguments.get_parameter --foo -f foo - >>> echo $foo - >>> echo "${arguments_new_arguments[@]}" - bar - other_param1 other_param2 - ' - local parameter_aliases parameter variable argument index match - parameter_aliases=($(array.slice :-1 "$@")) - variable="$(array.slice -1 "$@")" - match=false - local new_arguments=() - for index in "${!arguments_new_arguments[@]}"; do - argument="${arguments_new_arguments[$index]}" - $match && match=false && continue - match=false - for parameter in "${parameter_aliases[@]}"; do - if [[ "$argument" == "$parameter" ]]; then - eval "${variable}=${arguments_new_arguments[((index+1))]}" - match=true - break - fi - done - $match || new_arguments+=( "$argument" ) - done - arguments_new_arguments=( "${new_arguments[@]:+${new_arguments[@]}}" ) -} -arguments_get_positional() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - ``` - arguments.get_positional index variable_name - ``` - - Get the positional parameter at `index`. Use after extracting parameters, - keywords and flags. - - >>> arguments.set parameter foo --flag pos1 pos2 --keyword=foo - >>> arguments.get_flag --flag _ - >>> arguments.get_parameter parameter _ - >>> arguments.get_keyword --keyword _ - >>> local positional1 positional2 - >>> arguments.get_positional 1 positional1 - >>> arguments.get_positional 2 positional2 - >>> echo "$positional1 $positional2" - pos1 pos2 - ' - local index="$1" - (( index-- )) # $0 is not available here - local variable="$2" - eval "${variable}=${arguments_new_arguments[index]}" -} -arguments_apply_new_arguments() { - local __doc__=' - Call this function after you are finished with argument parsing. The - arguments array ($@) will then contain all unparsed arguments that are - left. - ' - # implemented as alias - true -} -alias arguments.apply_new_arguments='set -- "${arguments_new_arguments[@]}"' -alias arguments.set="arguments_set" -alias arguments.get_flag="arguments_get_flag" -alias arguments.get_keyword="arguments_get_keyword" -alias arguments.get_parameter="arguments_get_parameter" -alias arguments.get_positional="arguments_get_positional" diff --git a/array.sh b/array.sh deleted file mode 100644 index b7e1dc9..0000000 --- a/array.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source $(dirname ${BASH_SOURCE[0]})/core.sh -# shellcheck disable=SC2034 -array_get_index() { - # shellcheck disable=SC2016 - local __doc__=' - Get index of value in an array - - >>> local a=(one two three) - >>> array_get_index one "${a[@]}" - 0 - >>> local a=(one two three) - >>> array_get_index two "${a[@]}" - 1 - >>> array_get_index bar foo bar baz - 1 - ' - local value="$1" - shift - local array=("$@") - local -i index=-1 - local i - for i in "${!array[@]}"; do - if [[ "${array[$i]}" == "${value}" ]]; then - local index="${i}" - fi - done - echo "$index" - if (( index == -1 )); then - return 1 - fi -} -array_filter() { - # shellcheck disable=SC2016,SC2034 - local __doc__=' - Filters values from given array by given regular expression. - - >>> local a=(one two three wolf) - >>> local b=( $(array.filter ".*wo.*" "${a[@]}") ) - >>> echo ${b[*]} - two wolf - ' - local pattern="$1" - shift - local array=( $@ ) - local element - for element in "${array[@]}"; do - echo "$element" - done | grep --extended-regexp "$pattern" -} -array_slice() { - # shellcheck disable=SC2016,SC2034 - local __doc__=' - Returns a slice of an array (similar to Python). - - From the Python documentation: - One way to remember how slices work is to think of the indices as pointing - between elements, with the left edge of the first character numbered 0. - Then the right edge of the last element of an array of length n has - index n, for example: - ``` - +---+---+---+---+---+---+ - | 0 | 1 | 2 | 3 | 4 | 5 | - +---+---+---+---+---+---+ - 0 1 2 3 4 5 6 - -6 -5 -4 -3 -2 -1 - ``` - - >>> local a=(0 1 2 3 4 5) - >>> echo $(array.slice 1:-2 "${a[@]}") - 1 2 3 - >>> local a=(0 1 2 3 4 5) - >>> echo $(array.slice 0:1 "${a[@]}") - 0 - >>> local a=(0 1 2 3 4 5) - >>> [ -z "$(array.slice 1:1 "${a[@]}")" ] && echo empty - empty - >>> local a=(0 1 2 3 4 5) - >>> [ -z "$(array.slice 2:1 "${a[@]}")" ] && echo empty - empty - >>> local a=(0 1 2 3 4 5) - >>> [ -z "$(array.slice -2:-3 "${a[@]}")" ] && echo empty - empty - >>> local a=(0 1 2 3 4 5) - >>> [ -z "$(array.slice -2:-2 "${a[@]}")" ] && echo empty - empty - - Slice indices have useful defaults; an omitted first index defaults to - zero, an omitted second index defaults to the size of the string being - sliced. - >>> local a=(0 1 2 3 4 5) - >>> # from the beginning to position 2 (excluded) - >>> echo $(array.slice 0:2 "${a[@]}") - >>> echo $(array.slice :2 "${a[@]}") - 0 1 - 0 1 - - >>> local a=(0 1 2 3 4 5) - >>> # from position 3 (included) to the end - >>> echo $(array.slice 3:"${#a[@]}" "${a[@]}") - >>> echo $(array.slice 3: "${a[@]}") - 3 4 5 - 3 4 5 - - >>> local a=(0 1 2 3 4 5) - >>> # from the second-last (included) to the end - >>> echo $(array.slice -2:"${#a[@]}" "${a[@]}") - >>> echo $(array.slice -2: "${a[@]}") - 4 5 - 4 5 - - >>> local a=(0 1 2 3 4 5) - >>> echo $(array.slice -4:-2 "${a[@]}") - 2 3 - - If no range is given, it works like normal array indices. - >>> local a=(0 1 2 3 4 5) - >>> echo $(array.slice -1 "${a[@]}") - 5 - >>> local a=(0 1 2 3 4 5) - >>> echo $(array.slice -2 "${a[@]}") - 4 - >>> local a=(0 1 2 3 4 5) - >>> echo $(array.slice 0 "${a[@]}") - 0 - >>> local a=(0 1 2 3 4 5) - >>> echo $(array.slice 1 "${a[@]}") - 1 - >>> local a=(0 1 2 3 4 5) - >>> array.slice 6 "${a[@]}"; echo $? - 1 - >>> local a=(0 1 2 3 4 5) - >>> array.slice -7 "${a[@]}"; echo $? - 1 - ' - local start end array_length length - if [[ "$1" == *:* ]]; then - IFS=":"; read -r start end <<<"$1" - shift - array_length="$#" - # defaults - [ -z "$end" ] && end=$array_length - [ -z "$start" ] && start=0 - (( start < 0 )) && let "start=(( array_length + start ))" - (( end < 0 )) && let "end=(( array_length + end ))" - else - start="$1" - shift - array_length="$#" - (( start < 0 )) && let "start=(( array_length + start ))" - let "end=(( start + 1 ))" - fi - let "length=(( end - start ))" - (( start < 0 )) && return 1 - # check bounds - (( length < 0 )) && return 1 - (( start < 0 )) && return 1 - (( start >= array_length )) && return 1 - # parameters start with $1, so add 1 to $start - let "start=(( start + 1 ))" - echo "${@: $start:$length}" -} -alias array.slice="array_slice" -alias array.get_index="array_get_index" -alias array.filter="array_filter" diff --git a/btrfs.sh b/btrfs.sh deleted file mode 100644 index afefccb..0000000 --- a/btrfs.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source $(dirname ${BASH_SOURCE[0]})/core.sh - -core.import logging -core.import exceptions -core.import arguments - -#region doc test setup -btrfs__doc_test_setup__=' -# is run once before tests are started -core.import doc_test -doc_test_capture_stderr=false -mv() { - echo mv $@ -} -rmdir() { - echo rmdir $@ -} -pv() { - cat - | tr -d "\n" # print stdin - echo -n " | pv | " -} -btrfs() { - if [[ $1 == subvolume ]] && [[ $2 == snapshot ]]; then - shift - shift - echo btrfs subvolume snapshot $@ - fi - if [[ $1 == send ]]; then - shift - echo btrfs send $@ - fi - if [[ $1 == receive ]]; then - cat - # print stdin - shift - echo btrfs receive $@ - fi - if [[ $1 == subvolume ]] && [[ $2 == list ]] && \ - [[ "${!#}" == /broot ]] - then - echo '\'' ID 256 parent 5 top level 5 path __active - ID 259 parent 256 top level 256 path __active/var - ID 258 parent 256 top level 256 path __active/usr - ID 257 parent 256 top level 256 path __active/home - ID 1661 parent 5 top level 5 path __snapshot/backup_last - ID 1662 parent 1661 top level 1661 path __snapshot/backup_last/var - ID 1663 parent 1661 top level 1661 path __snapshot/backup_last/usr - ID 1664 parent 1661 top level 1661 path __snapshot/backup_last/home'\'' - fi - if [[ $1 == subvolume ]] && [[ $2 == show ]]; then - if [[ $3 == /broot ]]; then - echo "Name: " - echo "UUID: 123456ab-abc1-2345" - return 0 - fi - # check if subvolume - [[ $3 == /broot/__active ]] && return 0 - [[ $3 == /broot/__active/var ]] && return 0 - [[ $3 == /broot/__active/usr ]] && return 0 - [[ $3 == /broot/__active/home ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last/var ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last/usr ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last/home ]] && return 0 - # return error if not a subvolume - return 1 - fi - if [[ $1 == subvolume ]] && [[ $2 == delete ]]; then - # check if subvolume - [[ $3 == /broot/__active ]] && return 0 - [[ $3 == /broot/__active/var ]] && return 0 - [[ $3 == /broot/__active/usr ]] && return 0 - [[ $3 == /broot/__active/home ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last/var ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last/usr ]] && return 0 - [[ $3 == /broot/__snapshot/backup_last/home ]] && return 0 - # return error if not a subvolume - return 1 - fi -} -' -#endregion - -# region helper functions -btrfs_is_subvolume() { - local __doc__=' - Checks if path is a subvolume. Note: The btrfs root is also a subvolume. - >>> btrfs_is_subvolume /broot; echo $? - 0 - >>> btrfs_is_subvolume /broot/__active; echo $? - 0 - >>> btrfs_is_subvolume /broot/__active/usr; echo $? - 0 - >>> btrfs_is_subvolume /broot/__active/etc; echo $? - 1 - ' - btrfs subvolume show "$1" &>/dev/null -} -btrfs_is_btrfs_root() { - local __doc__=' - >>> btrfs_is_btrfs_root /broot; echo $? - 0 - >>> btrfs_is_btrfs_root /broot/foo; echo $? - 1 - ' - #btrfs subvolume show "$1" 1>&2 - (btrfs subvolume show "$1" | grep "is btrfs root") &>/dev/null || \ - (btrfs subvolume show "$1" | grep "is toplevel") &>/dev/null || \ - (btrfs subvolume show "$1" | grep "Name:.*") &>/dev/null || \ - return 1 -} -btrfs_find_root() { - local __doc__=' - Returns absolute path to btrfs root. - Example: - >>> btrfs_find_root /broot/__active - /broot - >>> btrfs_find_root /broot/__snapshot/backup_last - /broot - >>> btrfs_find_root /not/a/valid/mountpoint; echo $? - 1 - ' - local path="$1" - while true; do - btrfs_is_btrfs_root "$path" && echo "$path" && return 0 - [[ "$path" == "/" ]] && return 1 - path="$(dirname "$path")" - done -} - -btrfs_get_subvolume_list_field() { - local __doc__=' - >>> local entry="$(btrfs subvolume list /broot | head -n1)" - >>> btrfs_get_subvolume_list_field path "$entry" - >>> btrfs_get_subvolume_list_field ID "$entry" - >>> btrfs_get_subvolume_list_field parent "$entry" - __active - 256 - 5 - ' - local target="$1" - local entry=($2) - local found=false - local field - for field in "${entry[@]}"; do - $found && echo "$field" && break - # case insensitive match (bash >= 4) - [[ "${field,,}" == "${target,,}" ]] && found=true - done -} -btrfs_subvolume_filter() { - local __doc__=' - Example: - >>> btrfs_subvolume_filter /broot parent 256 - ID 259 parent 256 top level 256 path __active/var - ID 258 parent 256 top level 256 path __active/usr - ID 257 parent 256 top level 256 path __active/home - >>> btrfs_subvolume_filter /broot id 256 - ID 256 parent 5 top level 5 path __active - ' - local btrfs_root="$(realpath "$1")" - local target_key="$2" - local target_value="$3" - local entry - btrfs_is_btrfs_root "$btrfs_root" || return 1 - btrfs subvolume list -p "$btrfs_root" | while read -r entry; do - local value - value="$(btrfs_get_subvolume_list_field "$target_key" "$entry")" - if [[ "$value" == "$target_value" ]]; then - echo "$entry" - fi - done -} -btrfs_get_child_volumes() { - # shellcheck disable=SC2016 - local __doc__=' - Returns absolute paths to subvolumes - Example: - >>> btrfs_get_child_volumes /broot/__active - /broot/__active/var - /broot/__active/usr - /broot/__active/home - >>> btrfs_get_child_volumes /broot/__snapshot/backup_last - /broot/__snapshot/backup_last/var - /broot/__snapshot/backup_last/usr - /broot/__snapshot/backup_last/home - ' - local volume="$1" - local btrfs_root entry volume_id volume_relative - btrfs_is_subvolume "${volume}" || return 1 - btrfs_root="$(btrfs_find_root "$volume")" - volume_relative="$(core.rel_path "$btrfs_root" "$volume")" - entry="$( - btrfs_subvolume_filter "$btrfs_root" path "$volume_relative" - )" - volume_id="$(btrfs_get_subvolume_list_field id "$entry")" - btrfs_subvolume_filter "$btrfs_root" parent "$volume_id" \ - | while read -r entry - do - child_path="$(btrfs_get_subvolume_list_field path "$entry")" - echo "${btrfs_root}/${child_path}" - done -} -# endregion - -#region btrfs functions -btrfs_subvolume_delete() { - local __doc__=' - # Delete a subvolume. Also deletes child subvolumes. - >>> btrfs_subvolume_delete /broot/__snapshot/backup_last - >>> echo $? - 0 - >>> btrfs_subvolume_delete /broot/__snapshot/foo - >>> echo $? - 1 - ' - local volume="$1" - local child - btrfs_subvolume_set_ro "$volume" false - btrfs_get_child_volumes "$volume" \ - | while read -r child - do - btrfs subvolume delete "$child" - done - btrfs subvolume delete "$volume" -} -btrfs_subvolume_set_ro() { - local __doc__=' - # Make subvolume writable or readonly. Also applies to child subvolumes. - ' - local volume="$1" - local read_only="$2" - [ -z "$2" ] && read_only=true - # if setting to writable set top volume first - $read_only || btrfs property set -ts "$volume" ro $read_only - local child - btrfs_get_child_volumes "$volume" | while read -r child; do - btrfs property set -ts "$child" ro $read_only - done - # if setting to read_only set top volume last - if $read_only; then - btrfs property set -ts "$volume" ro $read_only - fi -} -btrfs_snapshot() { - local __doc__=' - # Make snapshot of subvolume. - - >>> btrfs_snapshot /broot/__active /backup/__active_backup - btrfs subvolume snapshot /broot/__active /backup/__active_backup - rmdir /backup/__active_backup/var - btrfs subvolume snapshot /broot/__active/var /backup/__active_backup/var - rmdir /backup/__active_backup/usr - btrfs subvolume snapshot /broot/__active/usr /backup/__active_backup/usr - rmdir /backup/__active_backup/home - btrfs subvolume snapshot /broot/__active/home /backup/__active_backup/home - - Third parameter can be used to exclude a subvolume (currently only one) - >>> btrfs_snapshot /broot/__active /backup/__active_backup usr - btrfs subvolume snapshot /broot/__active /backup/__active_backup - rmdir /backup/__active_backup/var - btrfs subvolume snapshot /broot/__active/var /backup/__active_backup/var - rmdir /backup/__active_backup/home - btrfs subvolume snapshot /broot/__active/home /backup/__active_backup/home - ' - local volume="$1" - local target="$2" - local exclude="$3" - btrfs subvolume snapshot "${volume}" "${target}" - local child child_relative - btrfs_get_child_volumes "$volume" | while read -r child; do - child_relative="$(core.rel_path "$volume" "$child")" - if [ "$child_relative" != "$exclude" ]; then - rmdir "${target}/${child_relative}" - btrfs subvolume snapshot "${child}" "${target}/${child_relative}" - fi - done -} -btrfs_send_update() { - # shellcheck disable=SC2034,SC1004 - local __doc__=' - # Update snapshot (needs backing snapshot). - e.g - >>> btrfs_send_update /broot/__active \ - >>> /broot/backing \ - >>> /backup - btrfs send -p /broot/backing /broot/__active | pv | btrfs receive /backup - rmdir /backup/__active/var - btrfs send -p /broot/backing/var /broot/__active/var | pv | btrfs receive /backup/__active - rmdir /backup/__active/usr - btrfs send -p /broot/backing/usr /broot/__active/usr | pv | btrfs receive /backup/__active - rmdir /backup/__active/home - btrfs send -p /broot/backing/home /broot/__active/home | pv | btrfs receive /backup/__active - ' - local volume="$1" - local volume_name="$(basename "$1")" - local backing_snapshot="$2" - local target="$3" - # Note btrfs send can only operate on read-only snapshots - btrfs_subvolume_set_ro "$volume" true - btrfs_subvolume_set_ro "$backing_snapshot" true - btrfs send -p "$backing_snapshot" "$volume" | \ - pv --progress --timer --rate --average-rate --bytes | \ - btrfs receive "$target" - # Note btrfs receive can only create the subdirs if not read-only - btrfs_subvolume_set_ro "${target}/${volume_name}" false - local child child_relative - btrfs_get_child_volumes "$volume" | while read -r child; do - child_relative="$(core.rel_path "$volume" "$child")" - rmdir "${target}/${volume_name}/${child_relative}" - btrfs send -p "${backing_snapshot}/${child_relative}" "$child" | \ - pv --progress --timer --rate --average-rate --bytes | \ - btrfs receive "${target}/${volume_name}" - done - btrfs_subvolume_set_ro "$volume" false -} -btrfs_send() { - local __doc__=' - # Send snapshot - >>> btrfs_send /broot/__active /backup/__active_backup - btrfs send /broot/__active | pv | btrfs receive /backup - btrfs send /broot/__active/var | pv | btrfs receive /backup/__active - btrfs send /broot/__active/usr | pv | btrfs receive /backup/__active - btrfs send /broot/__active/home | pv | btrfs receive /backup/__active - mv /backup/__active /backup/__active_backup - ' - local volume="$1" - local volume_name="$(basename "$1")" - local target="$2" - local target_dir="$(dirname "$2")" - local target_name="$(basename "$2")" - # Note btrfs send can only operate on read-only snapshots - btrfs_subvolume_set_ro "$volume" true - btrfs send "$volume" | \ - pv --progress --timer --rate --average-rate --bytes | \ - btrfs receive "$target_dir" - # Note btrfs receive can only create the subdirs if not read-only - btrfs_subvolume_set_ro "${target_dir}/$volume_name" false - local child - btrfs_get_child_volumes "$volume" \ - | while read -r child - do - btrfs send "$child" | \ - pv --progress --timer --rate --average-rate --bytes | \ - btrfs receive "${target_dir}/${volume_name}" - done - mv "${target_dir}/$volume_name" "$target" - btrfs_subvolume_set_ro "$volume" false -} -#endregion - -# region vim modline -# vim: set tabstop=4 shiftwidth=4 expandtab: -# vim: foldmethod=marker foldmarker=region,endregion: -# endregion diff --git a/change_root.sh b/change_root.sh deleted file mode 100644 index bc482da..0000000 --- a/change_root.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -# region imports -source "$(dirname "${BASH_SOURCE[0]}")/core.sh" -core.import logging -# endregion - -change_root_kernel_api_locations=(/proc /sys /sys/firmware/efi/efivars /dev \ - /dev/pts /dev/shm /run) -# TODO implement dependency check in import mechanism -change_root__dependencies__=(mountpoint mount umount mkdir) -change_root__optional_dependencies__=(fakeroot fakechroot) - -change_root() { - local __doc__=' - This function performs a linux change root if needed and provides all - kernel api filesystems in target root by using a change root interface - with minimal needed rights. - - #### Example: - - `change_root /new_root /usr/bin/env bash some arguments` - ' - if [[ "$1" == '/' ]]; then - shift - return $? - else - change_root_with_kernel_api "$@" - return $? - fi - return $? -} - -change_root_with_fake_fallback() { - local __doc__=' - Perform the available change root program wich needs at least rights. - - #### Example: - - `change_root_with_fake_fallback /new_root /usr/bin/env bash some arguments` - ' - if [[ "$UID" == '0' ]]; then - chroot "$@" - return $? - fi - fakeroot fakechroot chroot "$@" - return $? -} - -change_root_with_kernel_api() { - local __doc__=' - Performs a change root by mounting needed host locations in change root - environment. - - #### Example: - - `change_root_with_kernel_api /new_root /usr/bin/env bash some arguments` - ' - local new_root_location="$1" - if [[ ! "$new_root_location" =~ .*/$ ]]; then - new_root_location+='/' - fi - local mountpoint_path - for mountpoint_path in ${change_root_kernel_api_locations[*]}; do - mountpoint_path="${mountpoint_path:1}" - # TODO fix - #./build-initramfs.sh -d -p ../../initramfs -s -t /mnt/old - #mkdir: cannot create directory ‘/mnt/old/sys/firmware/efi’: No such file or directory - #Traceback (most recent call first): - #[0] /srv/openslx-ng/systemd-init/builder/dnbd3-rootfs/scripts/rebash/change_root.sh:67: change_root_with_kernel_api - #[1] /srv/openslx-ng/systemd-init/builder/dnbd3-rootfs/scripts/rebash/change_root.sh:28: change_root - #[2] ./build-initramfs.sh:532: main - #[3] ./build-initramfs.sh:625: main - if [ ! -e "${new_root_location}${mountpoint_path}" ]; then - mkdir --parents "${new_root_location}${mountpoint_path}" - # TODO remember created dirs. - fi - if ! mountpoint -q "${new_root_location}${mountpoint_path}"; then - if [ "$mountpoint_path" == 'proc' ]; then - mount "/${mountpoint_path}" \ - "${new_root_location}${mountpoint_path}" --types \ - "$mountpoint_path" --options nosuid,noexec,nodev - elif [ "$mountpoint_path" == 'sys' ]; then - mount "/${mountpoint_path}" \ - "${new_root_location}${mountpoint_path}" --types sysfs \ - --options nosuid,noexec,nodev - elif [ "$mountpoint_path" == 'dev' ]; then - mount udev "${new_root_location}${mountpoint_path}" --types \ - devtmpfs --options mode=0755,nosuid - elif [ "$mountpoint_path" == 'dev/pts' ]; then - mount devpts "${new_root_location}${mountpoint_path}" \ - --types devpts --options mode=0620,gid=5,nosuid,noexec - elif [ "$mountpoint_path" == 'dev/shm' ]; then - mount shm "${new_root_location}${mountpoint_path}" --types \ - tmpfs --options mode=1777,nosuid,nodev - elif [ "$mountpoint_path" == 'run' ]; then - mount "/${mountpoint_path}" \ - "${new_root_location}${mountpoint_path}" --types tmpfs \ - --options nosuid,nodev,mode=0755 - elif [ "$mountpoint_path" == 'tmp' ]; then - mount run "${new_root_location}${mountpoint_path}" --types \ - tmpfs --options mode=1777,strictatime,nodev,nosuid - elif [ -f "/${mountpoint_path}" ]; then - mount "/${mountpoint_path}" \ - "${new_root_location}${mountpoint_path}" --bind - else - logging.warn \ - "Mountpoint \"/${mountpoint_path}\" couldn't be handled." - fi - fi - done - change_root_with_fake_fallback "$@" - local return_code=$? - # Reverse mountpoint list to unmount them in reverse order. - local reverse_kernel_api_locations - for mountpoint_path in ${reverse_kernel_api_locations[*]}; do - reverse_kernel_api_locations="$mountpoint_path ${reverse_kernel_api_locations[*]}" - done - for mountpoint_path in ${reverse_kernel_api_locations[*]}; do - mountpoint_path="${mountpoint_path:1}" && \ - if mountpoint -q "${new_root_location}${mountpoint_path}" || \ - [ -f "/${mountpoint_path}" ] - then - # If unmounting doesn't work try to unmount in lazy mode (when - # mountpoints are not needed anymore). - if ! umount "${new_root_location}${mountpoint_path}"; then - logging.warn "Unmounting \"${new_root_location}${mountpoint_path}\" fails so unmount it in force mode." - if ! umount -f "${new_root_location}${mountpoint_path}"; then - logging.warn "Unmounting \"${new_root_location}${mountpoint_path}\" in force mode fails so unmount it if mountpoint isn't busy anymore." - umount -l "${new_root_location}${mountpoint_path}" - fi - fi - # NOTE: "return_code" remains with an error code if there was - # given one in all iterations. - [[ $? != 0 ]] && return_code=$? - else - logging.warn \ - "Location \"${new_root_location}${mountpoint_path}\" should be a mountpoint but isn't." - fi - done - return $return_code -} - -alias change_root.kernel_api_locations=change_root_kernel_api_locations -alias change_root.with_fake_fallback=change_root_with_fake_fallback -alias change_root.with_kernel_api=change_root_with_kernel_api diff --git a/core.sh b/core.sh deleted file mode 100644 index 7747928..0000000 --- a/core.sh +++ /dev/null @@ -1,406 +0,0 @@ -#!/usr/bin/env bash -if [ ${#core_imported_modules[@]} -ne 0 ]; then - # load core only once - return 0 -fi - -shopt -s expand_aliases -#TODO use set -o nounset - -core_is_main() { - local __doc__=' - Returns true if current script is being executed. - - >>> # Note: this test passes because is_main is called by doc_test.sh which - >>> # is being executed. - >>> core.is_main && echo yes - yes - ' - [[ "${BASH_SOURCE[1]}" = "$0" ]] -} -core_abs_path() { - local path="$1" - if [ -d "$path" ]; then - local abs_path_dir - abs_path_dir="$(cd "$path" && pwd)" - echo "${abs_path_dir}" - else - local file_name - local abs_path_dir - file_name="$(basename "$path")" - path=$(dirname "$path") - abs_path_dir="$(cd "$path" && pwd)" - echo "${abs_path_dir}/${file_name}" - fi -} -core_rel_path() { - # shellcheck disable=SC2016 - local __doc__=' - Computes relative path from $1 to $2. - Taken from http://stackoverflow.com/a/12498485/2972353 - - >>> core_rel_path "/A/B/C" "/A" - ../.. - >>> core_rel_path "/A/B/C" "/A/B" - .. - >>> core_rel_path "/A/B/C" "/A/B/C/D" - D - >>> core_rel_path "/A/B/C" "/A/B/C/D/E" - D/E - >>> core_rel_path "/A/B/C" "/A/B/D" - ../D - >>> core_rel_path "/A/B/C" "/A/B/D/E" - ../D/E - >>> core_rel_path "/A/B/C" "/A/D" - ../../D - >>> core_rel_path "/A/B/C" "/A/D/E" - ../../D/E - >>> core_rel_path "/A/B/C" "/D/E/F" - ../../../D/E/F - >>> core_rel_path "/" "/" - . - >>> core_rel_path "/A/B/C" "/A/B/C" - . - >>> core_rel_path "/A/B/C" "/" - ../../../ - ' - # both $1 and $2 are absolute paths beginning with / - # returns relative path to $2/$target from $1/$source - local source="$1" - local target="$2" - if [[ "$source" == "$target" ]]; then - echo "." - return - fi - - local common_part="$source" # for now - local result="" # for now - - while [[ "${target#$common_part}" == "${target}" ]]; do - # no match, means that candidate common part is not correct - # go up one level (reduce common part) - common_part="$(dirname "$common_part")" - # and record that we went back, with correct / handling - if [[ -z $result ]]; then - result=".." - else - result="../$result" - fi - done - - if [[ $common_part == "/" ]]; then - # special case for root (no common path) - result="$result/" - fi - - # since we now have identified the common part, - # compute the non-common part - local forward_part="${target#$common_part}" - - # and now stick all parts together - if [[ -n $result ]] && [[ -n $forward_part ]]; then - result="$result$forward_part" - elif [[ -n $forward_part ]]; then - # extra slash removal - result="${forward_part:1}" - fi - echo "$result" -} - -core_imported_modules=("$(core_abs_path "${BASH_SOURCE[0]}")") -core_imported_modules+=("$(core_abs_path "${BASH_SOURCE[1]}")") -core_declarations_before="" -core_declared_functions_after_import="" -core_import_level=0 - -core_log() { - if type -t logging_log > /dev/null; then - logging_log "$@" - else - local level=$1 - shift - echo "$level": "$@" - fi -} -core_is_empty() { - local __doc__=' - Tests if variable is empty (undefined variables are not empty) - - >>> local foo="bar" - >>> core_is_empty foo; echo $? - 1 - >>> local defined_and_empty="" - >>> core_is_empty defined_and_empty; echo $? - 0 - >>> core_is_empty undefined_variable; echo $? - 1 - - >>> set -u - >>> core_is_empty undefined_variable; echo $? - 1 - ' - local variable_name="$1" - core_is_defined "$variable_name" || return 1 - [ -z "${!variable_name}" ] || return 1 -} -core_is_defined() { - # shellcheck disable=SC2034 - local __doc__=' - Tests if variable is defined (can also be empty) - - >>> local foo="bar" - >>> core_is_defined foo; echo $? - >>> [[ -v foo ]]; echo $? - 0 - 0 - >>> local defined_but_empty="" - >>> core_is_defined defined_but_empty; echo $? - 0 - >>> core_is_defined undefined_variable; echo $? - 1 - >>> set -o nounset - >>> core_is_defined undefined_variable; echo $? - 1 - - Same Tests for bash < 4.3 - >>> core__bash_version_test=true - >>> local foo="bar" - >>> core_is_defined foo; echo $? - 0 - >>> core__bash_version_test=true - >>> local defined_but_empty="" - >>> core_is_defined defined_but_empty; echo $? - 0 - >>> core__bash_version_test=true - >>> core_is_defined undefined_variable; echo $? - 1 - >>> core__bash_version_test=true - >>> set -o nounset - >>> core_is_defined undefined_variable; echo $? - 1 - ' - ( - set +o nounset - if ((BASH_VERSINFO[0] >= 4)) && ((BASH_VERSINFO[1] >= 3)) \ - && [ -z "${core__bash_version_test:-}" ]; then - [[ -v "${1:-}" ]] || exit 1 - else # for bash < 4.3 - # Note: ${varname:-foo} expands to foo if varname is unset or set to the - # empty string; ${varname-foo} only expands to foo if varname is unset. - # shellcheck disable=SC2016 - eval '! [[ "${'"${1}"'-this_variable_is_undefined_!!!}"' \ - ' == "this_variable_is_undefined_!!!" ]]' - exit $? - fi - ) -} -core_get_all_declared_names() { - # shellcheck disable=SC2016 - local __doc__=' - Return all declared variables and function in the current scope. - - E.g. - `declarations="$(core.get_all_declared_names)"` - ' - local only_functions="${1:-}" - [ -z "$only_functions" ] && only_functions=false - { - declare -F | cut --delimiter ' ' --fields 3 - $only_functions || declare -p | grep '^declare' \ - | cut --delimiter ' ' --fields 3 - | cut --delimiter '=' --fields 1 - } | sort --unique -} -core_get_all_aliases() { - local __doc__=' - Returns all defined aliases in the current scope. - ' - alias | grep '^alias' \ - | cut --delimiter ' ' --fields 2 - | cut --delimiter '=' --fields 1 -} -core_source_with_namespace_check() { - local __doc__=' - Sources a script and checks variable definitions before and after sourcing. - ' - # TODO make sure sourcing a file does not change the value of already - # defined variables. - local module_path="$1" - local namespace="$2" - local declarations_after declarations_diff - [ "$core_import_level" = '0' ] && \ - core_declared_functions_before="$(mktemp --suffix=rebash-core-before)" - core_get_all_declared_names true > "$core_declared_functions_before" - declarations_after="$(mktemp --suffix=rebash-core-dec-after)" - if [ "$core_declarations_before" = "" ]; then - core_declarations_before="$(mktemp --suffix=rebash-core-dec)" - fi - # region check if namespace clean before sourcing - local variable_or_function core_variable - core_get_all_declared_names > "$core_declarations_before" - while read -r variable_or_function ; do - if [[ $variable_or_function =~ ^${namespace}[._]* ]]; then - core_log warn "Namespace '$namespace' is not clean:" \ - "'$variable_or_function' is defined" 1>&2 - fi - done < "$core_declarations_before" - # endregion - - core_import_level=$((core_import_level+1)) - # shellcheck disable=1090 - source "$module_path" - [ $? = 1 ] && core_log critical "Failed to source $module_path" && exit 1 - core_import_level=$((core_import_level-1)) - - # check if sourcing defined unprefixed names - core_get_all_declared_names > "$declarations_after" - if ! $core_suppress_declaration_warning; then - declarations_diff="$(! diff "$core_declarations_before" \ - "$declarations_after" | grep -e "^>" | sed 's/^> //')" - for variable_or_function in $declarations_diff; do - if ! [[ $variable_or_function =~ ^${namespace}[._]* ]]; then - core_log warn "module \"$namespace\" defines unprefixed" \ - "name: \"$variable_or_function\"" 1>&2 - fi - done - fi - core_get_all_declared_names > "$core_declarations_before" - if [ "$core_import_level" = '0' ]; then - rm "$core_declarations_before" - core_declarations_before="" - core_declared_functions_after="$(mktemp --suffix=rebash-core-after)" - core_get_all_declared_names true > "$core_declared_functions_after" - core_declared_functions_after_import="$(! diff \ - "$core_declared_functions_before" \ - "$core_declared_functions_after" \ - | grep '^>' | sed 's/^> //' - )" - rm "$core_declared_functions_after" - rm "$core_declared_functions_before" - fi - if (( core_import_level == 1 )); then - declare -F | cut --delimiter ' ' --fields 3 \ - > "$core_declared_functions_before" - fi - rm "$declarations_after" -} -core_suppress_declaration_warning=false -core_import() { - # shellcheck disable=SC2016,SC1004 - local __doc__=' - IMPORTANT: Do not use core.import inside functions -> aliases do not work - TODO: explain this in more detail - - >>> ( - >>> core.import logging - >>> logging_set_level warn - >>> core.import test/mockup_module-b.sh false - >>> ) - +doc_test_contains - imported module c - module "mockup_module_c" defines unprefixed name: "foo123" - imported module b - - Modules should be imported only once. - >>> (core.import test/mockup_module_a.sh && \ - >>> core.import test/mockup_module_a.sh) - imported module a - - >>> ( - >>> core.import test/mockup_module_a.sh false - >>> echo $core_declared_functions_after_import - >>> ) - imported module a - mockup_module_a_foo - - >>> ( - >>> core.import logging - >>> logging_set_level warn - >>> core.import test/mockup_module_c.sh false - >>> echo $core_declared_functions_after_import - >>> ) - +doc_test_contains - imported module b - imported module c - module "mockup_module_c" defines unprefixed name: "foo123" - foo123 - - ' - local module="$1" - local suppress_declaration_warning="${2:-}" - # If "$suppress_declaration_warning" is empty do not change the current value - # of "$core_suppress_declaration_warning". (So it is not changed by nested - # imports.) - if [[ "$suppress_declaration_warning" == "true" ]]; then - core_suppress_declaration_warning=true - elif [[ "$suppress_declaration_warning" == "false" ]]; then - core_suppress_declaration_warning=false - fi - local module_path="" - local path - # shellcheck disable=SC2034 - core_declared_functions_after_import="" - - path="$(core_abs_path "$(dirname "${BASH_SOURCE[0]}")")" - local caller_path - caller_path="$(core_abs_path "$(dirname "${BASH_SOURCE[1]}")")" - # try absolute - if [[ $module == /* ]] && [[ -e "$module" ]];then - module_path="$module" - fi - # try relative - if [[ -f "${caller_path}/${module}" ]]; then - module_path="${caller_path}/${module}" - fi - # try rebash modules - if [[ -f "${path}/${module%.sh}.sh" ]]; then - module_path="${path}/${module%.sh}.sh" - fi - - if [ "$module_path" == "" ]; then - core_log critical "failed to import \"$module\"" - return 1 - fi - - module="$(basename "$module_path")" - - # normalize module_path - module_path="$(core.abs_path "$module_path")" - # check if module already loaded - local loaded_module - for loaded_module in "${core_imported_modules[@]}"; do - if [[ "$loaded_module" == "$module_path" ]];then - (( core_import_level == 0 )) && \ - core_declarations_before='' - return 0 - fi - done - - core_imported_modules+=("$module_path") - core_source_with_namespace_check "$module_path" "${module%.sh}" -} -core_unique() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - >>> local foo="a\nb\na\nb\nc\nb\nc" - >>> echo -e "$foo" | core.unique - a - b - c - ' - nl "$@" | sort --key 2 | uniq --skip-fields 1 | sort --numeric-sort | \ - sed 's/\s*[0-9]\+\s\+//' -} -alias core.import="core_import" -alias core.abs_path="core_abs_path" -alias core.rel_path="core_rel_path" -alias core.is_main="core_is_main" -alias core.get_all_declared_names="core_get_all_declared_names" -alias core.unique="core_unique" -alias core.is_defined="core_is_defined" -alias core.is_empty="core_is_empty" - -# region vim modline - -# vim: set tabstop=4 shiftwidth=4 expandtab: -# vim: foldmethod=marker foldmarker=region,endregion: - -# endregion diff --git a/dictionary.sh b/dictionary.sh deleted file mode 100644 index 22302c3..0000000 --- a/dictionary.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source "$(dirname "${BASH_SOURCE[0]}")/core.sh" -core.import logging - -dictionary_set() { - # shellcheck disable=SC2016 - local __doc__=' - Usage: `dictionary.set dictionary_name key value` - - #### Tests - - >>> dictionary_set map foo 2 - >>> echo ${dictionary__store_map[foo]} - 2 - >>> dictionary_set map foo "a b c" bar 5 - >>> echo ${dictionary__store_map[foo]} - >>> echo ${dictionary__store_map[bar]} - a b c - 5 - >>> dictionary_set map foo "a b c" bar; echo $? - 1 - - >>> dictionary__bash_version_test=true - >>> dictionary_set map foo 2 - >>> echo $dictionary__store_map_foo - 2 - >>> dictionary__bash_version_test=true - >>> dictionary_set map foo "a b c" - >>> echo $dictionary__store_map_foo - a b c - ' - local name="$1" - while true; do - local key="$2" - local value="\"$3\"" - shift 2 - (( $# % 2 )) || return 1 - # shellcheck disable=SC2154 - if [[ ${BASH_VERSINFO[0]} -lt 4 ]] \ - || ! [ -z "${dictionary__bash_version_test:-}" ]; then - eval "dictionary__store_${name}_${key}=""$value" - else - declare -Ag "dictionary__store_${name}" - eval "dictionary__store_${name}[${key}]=""$value" - fi - (( $# == 1 )) && return - done -} -dictionary_get_keys() { - local __doc__=' - Get keys of a dictionary as array. - - Usage: `dictionary.get_keys dictionary_name` - - - >>> dictionary_set map foo "a b c" bar 5 - >>> dictionary_get_keys map - bar - foo - - Iterate keys: - >>> dictionary_set map foo "a b c" bar 5 - >>> local key - >>> for key in $(dictionary_get_keys map); do - >>> echo "$key": "$(dictionary_get map "$key")" - >>> done - bar: 5 - foo: a b c - - >>> dictionary__bash_version_test=true - >>> dictionary_set map foo "a b c" bar 5 - >>> dictionary_get_keys map | sort -u - bar - foo - ' - local name="$1" - local keys key - local store='dictionary__store_'"${name}" - if [[ ${BASH_VERSINFO[0]} -lt 4 ]] \ - || ! [ -z "${dictionary__bash_version_test:-}" ]; then - for key in $(declare -p | cut -d' ' -f3 \ - | grep -E "^${store}" | cut -d '=' -f1); do - echo "${key#${store}_}" - done - else - # shellcheck disable=SC2016 - eval 'keys="${!'"$store"'[@]}"' - fi - # shellcheck disable=SC2154 - for key in ${keys:-}; do - echo "$key" - done -} -dictionary_get() { - # shellcheck disable=SC2034,2016 - local __doc__=' - Usage: `variable=$(dictionary.get dictionary_name key)` - - #### Examples - - >>> dictionary_get unset_map unset_value; echo $? - 1 - >>> dictionary__bash_version_test=true - >>> dictionary_get unset_map unset_value; echo $? - 1 - - >>> dictionary_set map foo 2 - >>> dictionary_set map bar 1 - >>> dictionary_get map foo - >>> dictionary_get map bar - 2 - 1 - - >>> dictionary_set map foo "a b c" - >>> dictionary_get map foo - a b c - - >>> dictionary__bash_version_test=true - >>> dictionary_set map foo 2 - >>> dictionary_get map foo - 2 - - >>> dictionary__bash_version_test=true - >>> dictionary_set map foo "a b c" - >>> dictionary_get map foo - a b c - ' - local name="$1" - local key="$2" - if [[ ${BASH_VERSINFO[0]} -lt 4 ]] \ - || ! [ -z "${dictionary__bash_version_test:-}" ]; then - local store="dictionary__store_${name}_${key}" - else - local store="dictionary__store_${name}[${key}]" - fi - core_is_defined "${store}" || return 1 - local value="${!store}" - echo "$value" -} -alias dictionary.set='dictionary_set' -alias dictionary.get='dictionary_get' -alias dictionary.get_keys='dictionary_get_keys' diff --git a/doc_test.sh b/doc_test.sh deleted file mode 100755 index 70ea362..0000000 --- a/doc_test.sh +++ /dev/null @@ -1,707 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source "$(dirname "$(readlink --canonicalize "${BASH_SOURCE[0]}")")/core.sh" - -core.import logging -core.import ui -core.import exceptions -core.import utils -core.import arguments -core.import time -core.import documentation -core.import utils -# region doc -# shellcheck disable=SC2034,SC2016 -doc_test__doc__=' - The doc_test module implements function and module level testing via "doc - strings". - - Tests can be run by invoking `doc_test.sh file1 folder1 file2 ...`. - - #### Options: - ``` - --help|-h Print help message. - --side-by-side Print diff of failing tests side by side. - --no-check-namespace Do not warn about unprefixed definitions. - --no-check-undocumented Do not warn about undocumented functions. - --use-nounset Accessing undefined variables produces error. - --verbose|-v Be more verbose - ``` - - #### Example output `./doc_test.sh -v arguments.sh` - ```bash - [verbose:doc_test.sh:330] arguments:[PASS] - [verbose:doc_test.sh:330] arguments_get_flag:[PASS] - [verbose:doc_test.sh:330] arguments_get_keyword:[PASS] - [verbose:doc_test.sh:330] arguments_get_parameter:[PASS] - [verbose:doc_test.sh:330] arguments_get_positional:[PASS] - [verbose:doc_test.sh:330] arguments_set:[PASS] - [info:doc_test.sh:590] arguments - passed 6/6 tests in 918 ms - [info:doc_test.sh:643] Total: passed 1/1 modules in 941 ms - ``` - - A doc string can be defined for a function by defining a variable named - `__doc__` at the function scope. - On the module level, the variable name should be `__doc__` - (e.g. `arguments__doc__` for the example above). - Note: The doc string needs to be defined with single quotes. - - Code contained in a module level variable named - `__doc_test_setup__` will be run once before all the Tests of - a module are run. This is usefull for defining mockup functions/data - that can be used throughout all tests. - - +documentation_exclude_print - #### Tests - - Tests are delimited by blank lines: - >>> echo bar - bar - - >>> echo $(( 1 + 2 )) - 3 - - But can also occur right after another: - >>> echo foo - foo - >>> echo bar - bar - - Single quotes can be escaped like so: - >>> echo '"'"'$foos'"'"' - $foos - - Or so - >>> echo '\''$foos'\'' - $foos - - Some text in between. - - Multiline output - >>> local i - >>> for i in 1 2; do - >>> echo $i; - >>> done - 1 - 2 - - Ellipsis support - >>> local i - >>> for i in 1 2 3 4 5; do - >>> echo $i; - >>> done - +doc_test_ellipsis - 1 - 2 - ... - - Ellipsis are non greedy - >>> local i - >>> for i in 1 2 3 4 5; do - >>> echo $i; - >>> done - +doc_test_ellipsis - 1 - ... - 4 - 5 - - Each testcase has its own scope: - >>> local testing="foo"; echo $testing - foo - >>> [ -z "${testing:-}" ] && echo empty - empty - - Syntax error in testcode: - >>> f() {a} - +doc_test_contains - +doc_test_ellipsis - syntax error near unexpected token `{a} - ... - - -documentation_exclude_print -' -# endregion -doc_test_compare_result() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - >>> local buffer="line 1 - >>> line 2" - >>> local got="line 1 - >>> line 2" - >>> doc_test_compare_result "$buffer" "$got"; echo $? - 0 - >>> local buffer="line 1 - >>> foo" - >>> local got="line 1 - >>> line 2" - >>> doc_test_compare_result "$buffer" "$got"; echo $? - 1 - >>> local buffer="+doc_test_contains - >>> line - >>> line" - >>> local got="line 1 - >>> line 2" - >>> doc_test_compare_result "$buffer" "$got"; echo $? - 0 - >>> local buffer="+doc_test_contains - >>> line - >>> foo" - >>> local got="line 1 - >>> line 2" - >>> doc_test_compare_result "$buffer" "$got"; echo $? - 1 - >>> local buffer="+doc_test_ellipsis - >>> line - >>> ... - >>> " - >>> local got="line - >>> line 2 - >>> " - >>> doc_test_compare_result "$buffer" "$got"; echo $? - 0 - >>> local buffer="+doc_test_ellipsis - >>> line - >>> ... - >>> line 2 - >>> " - >>> local got="line - >>> ignore - >>> ignore - >>> line 2 - >>> " - >>> doc_test_compare_result "$buffer" "$got"; echo $? - 0 - >>> local buffer="+doc_test_ellipsis - >>> line - >>> ... - >>> line 2 - >>> " - >>> local got="line - >>> ignore - >>> ignore - >>> line 2 - >>> line 3 - >>> " - >>> doc_test_compare_result "$buffer" "$got"; echo $? - 1 - ' - local buffer="$1" - local got="$2" - local buffer_line - local got_line - doc_test_compare_lines () { - if $doc_test_contains; then - [[ "$got_line" == *"$buffer_line"* ]] || return 1 - else - [[ "$buffer_line" == "$got_line" ]] || return 1 - fi - } - local result=0 - local doc_test_contains=false - local doc_test_ellipsis=false - local doc_test_ellipsis_on=false - local doc_test_ellipsis_waiting=false - local end_of_buffer=false - local end_of_got=false - while true; do - # parse buffer line - if ! $doc_test_ellipsis_waiting && ! $end_of_buffer && ! read -r -u3 buffer_line; then - end_of_buffer=true - fi - if [[ "$buffer_line" == "+doc_test_no_capture_stderr"* ]]; then - continue - fi - if [[ "$buffer_line" == "+doc_test_contains"* ]]; then - doc_test_contains=true - continue - fi - if [[ "$buffer_line" == "+doc_test_ellipsis"* ]]; then - doc_test_ellipsis=true - continue - fi - # parse got line - if $end_of_got || ! read -r -u4 got_line; then - end_of_got=true - fi - - # set result - if $doc_test_ellipsis;then - if [[ "$buffer_line" == "..." ]]; then - doc_test_ellipsis_on=true - else - [[ "$buffer_line" != "" ]] && $doc_test_ellipsis_on && doc_test_ellipsis_waiting=true - fi - fi - $end_of_buffer && $end_of_got && break - $end_of_buffer && $doc_test_ellipsis_waiting && result=1 && break - $end_of_got && $doc_test_ellipsis_waiting && result=1 && break - $end_of_buffer && $doc_test_ellipsis_on && break - if $doc_test_ellipsis_on; then - if doc_test_compare_lines; then - doc_test_ellipsis_on=false - doc_test_ellipsis_waiting=false - else - $end_of_got && result=1 - fi - else - doc_test_compare_lines || result=1 - fi - done 3<<< "$buffer" 4<<< "$got" - return $result -} -# shellcheck disable=SC2154 -doc_test_eval() { - local __doc__=' - >>> local test_buffer=" - >>> echo foo - >>> echo bar - >>> " - >>> local output_buffer="foo - >>> bar" - >>> doc_test_use_side_by_side_output=false - >>> doc_test_module_under_test=core - >>> doc_test_nounset=false - >>> doc_test_eval "$test_buffer" "$output_buffer" - ' - local test_buffer="$1" - [[ -z "$test_buffer" ]] && return 0 - local output_buffer="$2" - local text_buffer="${3-}" - local module="${4-}" - local function="${5-}" - local result=0 - local got declarations_before declarations_after - doc_test_eval_with_check() { - local test_buffer="$1" - local module="$2" - local function="$3" - local core_path="$(core_abs_path "$(dirname "${BASH_SOURCE[0]}")")/core.sh" - local setup_identifier="${module//[^[:alnum:]_]/_}"__doc_test_setup__ - local setup_string="${!setup_identifier:-}" - test_script="$( - echo '[ -z "$BASH_REMATCH" ] && BASH_REMATCH=""' - echo "source $core_path" - # Suppress the warnings here because they have been already been - # printed when analyzing the whole module - echo "core.import $doc_test_module_under_test true" - echo "$setup_string" - # _ can be used as anonymous variable (without warning) - echo '_=""' - echo "core.get_all_declared_names > $declarations_before" - $doc_test_nounset && echo 'set -o nounset' - # wrap in a function so the "local" keyword has an effect inside - # tests - echo " - _() { - $test_buffer - } - _ - " - echo "core.get_all_declared_names > $declarations_after" - )" - # run in clean environment - if echo "$output_buffer" | grep '+doc_test_no_capture_stderr' &>/dev/null; - then - #(eval "$test_script") - bash --noprofile --norc <(echo "$test_script") - else - #(eval "$test_script" 2>&1) - bash --noprofile --norc 2>&1 <(echo "$test_script") - fi - local result=$? - return $result - } - declarations_before="$(mktemp --suffix=rebash-doc_test)" - trap "rm -f $declarations_before; exit" EXIT - declarations_after="$(mktemp --suffix=rebash-doc_test)" - trap "rm -f $declarations_after; exit" EXIT - # TODO $module $function as parameters - got="$(doc_test_eval_with_check "$test_buffer" "$module" "$function")" - doc_test_declarations_diff="$(diff "$declarations_before" "$declarations_after" \ - | grep -e "^>" | sed 's/^> //')" - # TODO $module $function as parameters - doc_test_print_declaration_warning "$module" "$function" - rm "$declarations_before" - rm "$declarations_after" - if ! doc_test_compare_result "$output_buffer" "$got"; then - echo -e "${ui_color_lightred}test:${ui_color_default}" - echo "$test_buffer" - if $doc_test_use_side_by_side_output; then - output_buffer="expected"$'\n'"${output_buffer}" - got="got"$'\n'"${got}" - # TODO exclude doc_test_options - local diff=diff - utils.dependency_check colordiff && diff=colordiff - $diff --side-by-side <(echo "$output_buffer") <(echo "$got") - else - echo -e "${ui_color_lightred}expected:${ui_color_default}" - echo "$output_buffer" - echo -e "${ui_color_lightred}got:${ui_color_default}" - echo "$got" - fi - return 1 - fi -} -doc_test_run_test() { - local doc_string="$1" - local module="$2" - local function="$3" - local test_name="$module" - [[ -z "$function" ]] || test_name="$function" - if doc_test_parse_doc_string "$doc_string" doc_test_eval ">>>" \ - "$module" "$function" - then - logging.verbose "$test_name:[${ui_color_lightgreen}PASS${ui_color_default}]" - else - logging.warn "$test_name:[${ui_color_lightred}FAIL${ui_color_default}]" - return 1 - fi -} -doc_test_parse_doc_string() { - local __doc__=' - >>> local doc_string=" - >>> (test)block - >>> output block - >>> " - >>> _() { - >>> local output_buffer="$2" - >>> echo block: - >>> while read -r line; do - >>> if [ -z "$line" ]; then - >>> echo "empty_line" - >>> else - >>> echo "$line" - >>> fi - >>> done <<< "$output_buffer" - >>> } - >>> doc_test_parse_doc_string "$doc_string" _ "(test)" - block: - output block - - >>> local doc_string=" - >>> Some text (block 1). - >>> - >>> - >>> Some more text (block 1). - >>> (test)block 2 - >>> (test)block 2.2 - >>> output block 2 - >>> (test)block 3 - >>> output block 3 - >>> - >>> Even more text (block 4). - >>> " - >>> local i=0 - >>> _() { - >>> local test_buffer="$1" - >>> local output_buffer="$2" - >>> local text_buffer="$3" - >>> local line - >>> (( i++ )) - >>> echo "text_buffer (block $i):" - >>> if [ ! -z "$text_buffer" ]; then - >>> while read -r line; do - >>> if [ -z "$line" ]; then - >>> echo "empty_line" - >>> else - >>> echo "$line" - >>> fi - >>> done <<< "$text_buffer" - >>> fi - >>> echo "test_buffer (block $i):" - >>> [ ! -z "$test_buffer" ] && echo "$test_buffer" - >>> echo "output_buffer (block $i):" - >>> [ ! -z "$output_buffer" ] && echo "$output_buffer" - >>> return 0 - >>> } - >>> doc_test_parse_doc_string "$doc_string" _ "(test)" - text_buffer (block 1): - Some text (block 1). - empty_line - empty_line - Some more text (block 1). - test_buffer (block 1): - output_buffer (block 1): - text_buffer (block 2): - test_buffer (block 2): - block 2 - block 2.2 - output_buffer (block 2): - output block 2 - text_buffer (block 3): - test_buffer (block 3): - block 3 - output_buffer (block 3): - output block 3 - text_buffer (block 4): - Even more text (block 4). - test_buffer (block 4): - output_buffer (block 4): - - ' - local preserve_prompt - arguments.set "$@" - arguments.get_flag --preserve-prompt preserve_prompt - arguments.apply_new_arguments - local doc_string="$1" # the docstring to test - local parse_buffers_function="$2" - local prompt="$3" - local module="${4:-}" - local function="${5:-}" - [ -z "$prompt" ] && prompt=">>>" - local text_buffer="" - local test_buffer="" - local output_buffer="" - - # remove leading blank line - [[ "$(head --lines=1 <<< "$doc_string")" != *[![:space:]]* ]] && - doc_string="$(tail --lines=+2 <<< "$doc_string" )" - # remove trailing blank line - [[ "$(tail --lines=1 <<< "$doc_string")" != *[![:space:]]* ]] && - doc_string="$(head --lines=-1 <<< "$doc_string" )" - - doc_test_eval_buffers() { - $parse_buffers_function "$test_buffer" "$output_buffer" \ - "$text_buffer" "$module" "$function" - local result=$? - # clear buffers - text_buffer="" - test_buffer="" - output_buffer="" - return $result - } - local line - local state=TEXT - local next_state - local temp_prompt - #local indentation="" - while read -r line; do - #line="$(echo "$line" | sed -e 's/^[[:blank:]]*//')" # lstrip - case "$state" in - TEXT) - if [[ "$line" = "" ]]; then - next_state=TEXT - [ ! -z "$text_buffer" ] && text_buffer+=$'\n'"$line" - elif [[ "$line" = "$prompt"* ]]; then - next_state=TEST - [ ! -z "$text_buffer" ] && doc_test_eval_buffers - $preserve_prompt && temp_prompt="$prompt" && prompt="" - test_buffer="${line#$prompt}" - $preserve_prompt && prompt="$temp_prompt" - else - next_state=TEXT - # check if start of text - if [ -z "$text_buffer" ]; then - text_buffer="$line" - else - text_buffer+=$'\n'"$line" - fi - fi - ;; - TEST) - #[ -z "$indentation" ] && - #indentation="$(echo "$line"| grep -o "^[[:blank:]]*")" - if [[ "$line" = "" ]]; then - next_state=TEXT - doc_test_eval_buffers - [ $? == 1 ] && return 1 - elif [[ "$line" = "$prompt"* ]]; then - next_state=TEST - # check if start of test - $preserve_prompt && temp_prompt="$prompt" && prompt="" - if [ -z "$test_buffer" ]; then - test_buffer="${line#$prompt}" - else - test_buffer+=$'\n'"${line#$prompt}" - fi - $preserve_prompt && prompt="$temp_prompt" - else - next_state=OUTPUT - output_buffer="$line" - fi - ;; - OUTPUT) - if [[ "$line" = "" ]]; then - next_state=TEXT - doc_test_eval_buffers - [ $? == 1 ] && return 1 - elif [[ "$line" = "$prompt"* ]]; then - next_state=TEST - doc_test_eval_buffers - [ $? == 1 ] && return 1 - $preserve_prompt && temp_prompt="$prompt" && prompt="" - if [ -z "$test_buffer" ]; then - test_buffer="${line#$prompt}" - else - test_buffer+=$'\n'"${line#$prompt}" - fi - $preserve_prompt && prompt="$temp_prompt" - else - next_state=OUTPUT - # check if start of output - if [ -z "$output_buffer" ]; then - output_buffer="$line" - else - output_buffer+=$'\n'"$line" - fi - fi - ;; - esac - state=$next_state - done <<< "$doc_string" - # shellcheck disable=SC2154 - [[ "$(tail --lines=1 <<< "$text_buffer")" = "" ]] && - text_buffer="$(head --lines=-1 <<< "$text_buffer" )" - doc_test_eval_buffers -} -doc_test_doc_identifier=__doc__ -doc_test_doc_regex="/__doc__='/,/';$/p" -doc_test_doc_regex_one_line="__doc__='.*';$" -doc_test_get_function_docstring() { - function="$1" - ( - unset $doc_test_doc_identifier - if ! doc_string="$(type "$function" | \ - grep "$doc_test_doc_regex_one_line")" - then - doc_string="$(type "$function" | sed --quiet "$doc_test_doc_regex")" - fi - eval "$doc_string" - echo "${!doc_test_doc_identifier}" - ) -} -doc_test_print_declaration_warning() { - local module="$1" - local function="$2" - local test_name="$module" - [[ -z "$function" ]] || test_name="$function" - [[ "$doc_test_declarations_diff" == "" ]] && return - core.unique <<< "$doc_test_declarations_diff" \ - | while read -r variable_or_function - do - if ! [[ "$variable_or_function" =~ ^${module}[._]* ]]; then - logging.warn "Test '$test_name' defines unprefixed" \ - "name: '$variable_or_function'" - fi - done -} -doc_test_exceptions_active=false -doc_test_test_module() { - ( - module=$1 - core.import "$module" "$doc_test_supress_declaration" - doc_test_module_under_test="$(core.abs_path "$module")" - declared_functions="$core_declared_functions_after_import" - module="$(basename "$module")" - module="${module%.sh}" - declared_module_functions="$(! declare -F | cut -d' ' -f3 | grep -e "^${module%.sh}" )" - declared_functions="$declared_functions"$'\n'"$declared_module_functions" - declared_functions="$(core.unique <(echo "$declared_functions"))" - - local total=0 - local success=0 - time.timer_start - # module level tests - test_identifier="${module//[^[:alnum:]_]/_}"__doc__ - doc_string="${!test_identifier}" - if ! [ -z "$doc_string" ]; then - let "total++" - doc_test_run_test "$doc_string" "$module" && let "success++" - fi - # function level tests - # TODO detect and warn doc_strings with double quotes - test_identifier=__doc__ - for fun in $declared_functions; do - # shellcheck disable=SC2089 - doc_string="$(doc_test_get_function_docstring "$fun")" - if [[ "$doc_string" != "" ]]; then - let "total++" - doc_test_run_test "$doc_string" "$module" "$fun" && let "success++" - else - ! $doc_test_supress_undocumented && \ - logging.warn "undocumented function $fun" - fi - done - logging.info "$module - passed $success/$total tests in" \ - "$(time.timer_get_elapsed) ms" - (( success != total )) && exit 1 - exit 0 - ) -} -doc_test_parse_args() { - local __doc__=' - +documentation_exclude - >>> doc_test_parse_args non_existing_module - >>> echo $? - +doc_test_contains - +doc_test_ellipsis - Failed to test file: non_existing_module - ... - 1 - - -documentation_exclude - ' - local filename module directory verbose help - arguments.set "$@" - arguments.get_flag --help -h help - $help && documentation.print_doc_string "$doc_test__doc__" && return 0 - arguments.get_flag --side-by-side doc_test_use_side_by_side_output - # do not warn about unprefixed names - arguments.get_flag --no-check-namespace doc_test_supress_declaration - # do not warn about undocumented functions - arguments.get_flag --no-check-undocumented doc_test_supress_undocumented - # use set -o nounset inside tests - arguments.get_flag --use-nounset doc_test_nounset - arguments.get_flag --verbose -v verbose - arguments.apply_new_arguments - - if $verbose; then - logging.set_level verbose - else - logging.set_level info - fi - doc_test_test_directory() { - directory="$(core.abs_path "$1")" - for filename in "$directory"/*.sh; do - let "total++" - doc_test_test_module "$(core.abs_path "$filename")" & - done - } - time.timer_start - local total=0 - local success=0 - if [ $# -eq 0 ] || [ "$@" == "" ];then - doc_test_test_directory "$(dirname "$0")" - else - for filename in "$@"; do - if [ -f "$filename" ]; then - let "total++" - doc_test_test_module "$(core.abs_path "$filename")" & - elif [ -d "$filename" ]; then - doc_test_test_directory "$filename" - else - let "total++" - logging.warn "Failed to test file: $filename" - fi - done - fi - local job - for job in $(jobs -p); do - wait "$job" && let "success++" - done - logging.info "Total: passed $success/$total modules in" \ - "$(time.timer_get_elapsed) ms" - (( success != total )) && return 1 - return 0 -} - -if core.is_main; then - doc_test_parse_args "$@" -fi -# region vim modline - -# vim: set tabstop=4 shiftwidth=4 expandtab: -# vim: foldmethod=marker foldmarker=region,endregion: - -# endregion diff --git a/documentation.sh b/documentation.sh deleted file mode 100755 index 137f2ab..0000000 --- a/documentation.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source "$(dirname "$(readlink --canonicalize "${BASH_SOURCE[0]}")")/core.sh" - -core.import doc_test -core.import logging -core.import utils -core.import arguments -documentation_format_buffers() { - local buffer="$1" - local output_buffer="$2" - local text_buffer="$3" - [[ "$text_buffer" != "" ]] && echo "$text_buffer" - if [[ "$buffer" != "" ]]; then - # shellcheck disable=SC2016 - echo '```bash' - echo "$buffer" - echo "$output_buffer" - # shellcheck disable=SC2016 - echo '```' - fi -} -documentation_format_docstring() { - local doc_string="$1" - doc_string="$(echo "$doc_string" \ - | sed '/+documentation_exclude_print/d' \ - | sed '/-documentation_exclude_print/d' \ - | sed '/+documentation_exclude/,/-documentation_exclude/d')" - doc_test_parse_doc_string "$doc_string" documentation_format_buffers \ - --preserve-prompt -} -documentation_generate() { - # TODO add doc test setup function to documentation - module=$1 - ( - core.import "$module" || logging.warn "Failed to import module $module" - declared_functions="$core_declared_functions_after_import" - module="$(basename "$module")" - module="${module%.sh}" - declared_module_functions="$(! declare -F | cut -d' ' -f3 | grep -e "^${module%.sh}" )" - declared_functions="$declared_functions"$'\n'"$declared_module_functions" - declared_functions="$(core.unique <(echo "$declared_functions"))" - - # module level doc - test_identifier="$module"__doc__ - local doc_string="${!test_identifier}" - logging.plain "## Module $module" - if [[ -z "$doc_string" ]]; then - logging.warn "No top level documentation for module $module" 1>&2 - else - logging.plain "$(documentation_format_docstring "$doc_string")" - fi - - # function level documentation - test_identifier=__doc__ - local function - for function in $declared_functions; - do - # shellcheck disable=SC2089 - doc_string="$(doc_test_get_function_docstring "$function")" - if [[ -z "$doc_string" ]]; then - logging.warn "No documentation for function $function" 1>&2 - else - logging.plain "### Function $function" - logging.plain "$(documentation_format_docstring "$doc_string")" - fi - done - ) -} -documentation_serve() { - local __doc__=' - Serves a readme via webserver. Uses Flatdoc. - ' - local readme="$1" - [[ "$readme" == "" ]] && readme="README.md" - local server_root="$(mktemp --directory)" - cp "$readme" "$server_root/README.md" - pushd "$server_root" - wget --output-document index.html \ - https://cdn.rawgit.com/jandob/rebash/gh-pages/index-local.html - python2 -m SimpleHTTPServer 8080 - popd - rm -rf "$server_root" -} -documentation_parse_args() { - local filename module main_documentation serve - arguments.set "$@" - arguments.get_flag --serve serve - arguments.apply_new_arguments - $serve && documentation_serve "$1" && return 0 - main_documentation="$(dirname "${BASH_SOURCE[0]}")/rebash.md" - if [ $# -eq 0 ]; then - [[ -e "$main_documentation" ]] && cat "$main_documentation" - logging.plain "" - logging.plain "# Generated documentation" - for filename in $(dirname "$0")/*.sh; do - module=$(basename "${filename%.sh}") - documentation_generate "$module" - done - else - logging.plain "# Generated documentation" - for module in "$@"; do - documentation_generate "$(core_abs_path "$module")" - done - fi - return 0 -} -documentation_print_doc_string() { - local doc_string="$1" - echo "$doc_string" \ - | sed '/+documentation_exclude_print/,/-documentation_exclude_print/d' \ - | sed '/+documentation_exclude/,/-documentation_exclude/d' \ - | sed '/```/d' -} -alias documentation.print_doc_string="documentation_print_doc_string" - -if [[ ${BASH_SOURCE[0]} == "$0" ]]; then - logging.set_level debug - logging.set_commands_level info - documentation_parse_args "$@" -fi diff --git a/exceptions.sh b/exceptions.sh deleted file mode 100644 index 04342af..0000000 --- a/exceptions.sh +++ /dev/null @@ -1,329 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source "$(dirname "${BASH_SOURCE[0]}")/core.sh" -core.import logging - -# shellcheck disable=SC2034,SC2016 -exceptions__doc__=' - NOTE: The try block is executed in a subshell, so no outer variables can be - assigned. - - >>> exceptions.activate - >>> false - +doc_test_ellipsis - Traceback (most recent call first): - ... - - >>> exceptions_activate - >>> exceptions.try { - >>> false - >>> }; exceptions.catch { - >>> echo caught - >>> } - caught - - Exceptions in a subshell: - >>> exceptions_activate - >>> ( false ) - +doc_test_ellipsis - Traceback (most recent call first): - ... - Traceback (most recent call first): - ... - >>> exceptions_activate - >>> exceptions.try { - >>> (false; echo "this should not be printed") - >>> echo "this should not be printed" - >>> }; exceptions.catch { - >>> echo caught - >>> } - +doc_test_ellipsis - caught - - Nested exceptions: - >>> exceptions_foo() { - >>> true - >>> exceptions.try { - >>> false - >>> }; exceptions.catch { - >>> echo caught inside foo - >>> } - >>> false # this is cought at top level - >>> echo this should never be printed - >>> } - >>> - >>> exceptions.try { - >>> exceptions_foo - >>> }; exceptions.catch { - >>> echo caught - >>> } - >>> - caught inside foo - caught - - Exceptions are implicitely active inside try blocks: - >>> foo() { - >>> echo $1 - >>> true - >>> exceptions.try { - >>> false - >>> }; exceptions.catch { - >>> echo caught inside foo - >>> } - >>> false # this is not caught - >>> echo this should never be printed - >>> } - >>> - >>> foo "EXCEPTIONS NOT ACTIVE:" - >>> exceptions_activate - >>> foo "EXCEPTIONS ACTIVE:" - +doc_test_ellipsis - EXCEPTIONS NOT ACTIVE: - caught inside foo - this should never be printed - EXCEPTIONS ACTIVE: - caught inside foo - Traceback (most recent call first): - ... - - Exceptions inside conditionals: - >>> exceptions_activate - >>> false && echo "should not be printed" - >>> (false) && echo "should not be printed" - >>> exceptions.try { - >>> ( - >>> false - >>> echo "should not be printed" - >>> ) - >>> }; exceptions.catch { - >>> echo caught - >>> } - caught - - Print a caught exception traceback. - >>> exceptions.try { - >>> false - >>> }; exceptions.catch { - >>> echo caught - >>> echo "$exceptions_last_traceback" - >>> } - +doc_test_ellipsis - caught - Traceback (most recent call first): - ... - - Different syntax variations are possible. - >>> exceptions.try { - >>> ! true - >>> }; exceptions.catch { - >>> echo caught - >>> } - - >>> exceptions.try - >>> false - >>> exceptions.catch { - >>> echo caught - >>> } - caught - - >>> exceptions.try - >>> false - >>> exceptions.catch - >>> echo caught - caught - - >>> exceptions.try { - >>> false - >>> } - >>> exceptions.catch { - >>> echo caught - >>> } - caught - - >>> exceptions.try { - >>> false - >>> } - >>> exceptions.catch - >>> { - >>> echo caught - >>> } - caught - - Exceptions stay enabled after catch block. - >>> exceptions.activate - >>> exceptions.try { - >>> false - >>> }; exceptions.catch { - >>> echo caught - >>> } - >>> false - >>> echo "should not be printed" - caught - +doc_test_ellipsis - Traceback (most recent call first): - ... -' - -exceptions_active=false -exceptions_active_before_try=false -declare -ig exceptions_try_catch_level=0 - -exceptions_error_handler() { - local error_code=$? - local traceback="Traceback (most recent call first):" - local -i i=0 - while caller $i > /dev/null - do - local -a trace=( $(caller $i) ) - local line=${trace[0]} - local subroutine=${trace[1]} - local filename=${trace[2]} - traceback="$traceback"'\n'"[$i] ${filename}:${line}: ${subroutine}" - ((i++)) - done - if (( exceptions_try_catch_level == 0 )); then - logging_plain "$traceback" 1>&2 - else - logging_plain "$traceback" >"$exceptions_last_traceback_file" - fi - exit $error_code -} -exceptions_deactivate() { - # shellcheck disable=SC2016,2034 - local __doc__=' - >>> set -o errtrace - >>> trap '\''echo $foo'\'' ERR - >>> exceptions.activate - >>> trap -p ERR | cut --delimiter "'\''" --fields 2 - >>> exceptions.deactivate - >>> trap -p ERR | cut --delimiter "'\''" --fields 2 - exceptions_error_handler - echo $foo - ' - $exceptions_active || return 0 - [ "$exceptions_errtrace_saved" = "off" ] && set +o errtrace - [ "$exceptions_pipefail_saved" = "off" ] && set +o pipefail - [ "$exceptions_functrace_saved" = "off" ] && set +o functrace - export PS4="$exceptions_ps4_saved" - # shellcheck disable=SC2064 - trap "$exceptions_err_traps" ERR - exceptions_active=false -} - -exceptions_test_context() { - # shellcheck disable=SC2016,2034 - local __doc__=' - Note: set -e and ERR traps are prevented from working in a subshell if - it is disabled by the surrounding context. - >>> exceptions.activate - >>> exceptions_foo() { - >>> exceptions.try { - >>> false - >>> }; exceptions.catch { - >>> # this is not caught because of the || - >>> echo caught - >>> } - >>> false - >>> echo this should be printed - >>> } - >>> - >>> exceptions_foo || echo "error in exceptions_foo" - >>> - Warning: Context does not allow error trap! - this should be printed - ' - # test if context allows error traps - ( - local exceptions_test_context_pass=1 - set -o errtrace - trap 'exceptions_test_context_pass=0' ERR - false - [ $exceptions_test_context_pass == 1 ] && exit 1 - exit 0 - ) - return $? -} - -exceptions_activate() { - local do_not_check="$1" - if [ -z "$do_not_check" ]; then - exceptions_test_context - [ $? == 1 ] && logging_plain \ - "Warning: Context does not allow error trap!" 2>&1 - fi - $exceptions_active && return 0 - - exceptions_errtrace_saved=$(set -o | awk '/errtrace/ {print $2}') - exceptions_pipefail_saved=$(set -o | awk '/pipefail/ {print $2}') - exceptions_functrace_saved=$(set -o | awk '/functrace/ {print $2}') - exceptions_err_traps=$(trap -p ERR | cut --delimiter "'" --fields 2) - exceptions_ps4_saved="$PS4" - - # improve xtrace output (set -x) - export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' - - # If set, any trap on ERR is inherited by shell functions, - # command substitutions, and commands executed in a subshell environment. - # The ERR trap is normally not inherited in such cases. - set -o errtrace - # If set, any trap on DEBUG and RETURN are inherited by shell functions, - # command substitutions, and commands executed in a subshell environment. - # The DEBUG and RETURN traps are normally not inherited in such cases. - #set -o functrace - # If set, the return value of a pipeline is the value of the last - # (rightmost) command to exit with a non-zero status, or zero if all - # commands in the pipeline exit successfully. This option is disabled by - # default. - set -o pipefail - # Treat unset variables and parameters other than the special parameters - # ‘@’ or ‘*’ as an error when performing parameter expansion. - # An error message will be written to the standard error, and a - # non-interactive shell will exit. - #set -o nounset - - # traps: - # EXIT executed on shell exit - # DEBUG executed before every simple command - # RETURN executed when a shell function or a sourced code finishes executing - # ERR executed each time a command's failure would cause the shell to exit when the '-e' option ('errexit') is enabled - - # ERR is not executed in following cases: - # >>> err() { return 1;} - # >>> ! err - # >>> err || echo foo - # >>> err && echo foo - - trap exceptions_error_handler ERR - #trap exceptions_debug_handler DEBUG - #trap exceptions_exit_handler EXIT - exceptions_active=true -} - -exceptions_enter_try() { - if (( exceptions_try_catch_level == 0 )); then - exceptions_last_traceback_file="$(mktemp --suffix=rebash-exceptions)" - exceptions_active_before_try=$exceptions_active - fi - exceptions_deactivate - exceptions_try_catch_level+=1 -} - -exceptions_exit_try() { - local exceptions_result=$1 - exceptions_try_catch_level+=-1 - if (( exceptions_try_catch_level == 0 )); then - $exceptions_active_before_try && exceptions_activate 1 - exceptions_last_traceback="$( - logging.cat "$exceptions_last_traceback_file" - )" - rm "$exceptions_last_traceback_file" - else - exceptions_activate 1 - fi - return $exceptions_result -} - -alias exceptions.activate="exceptions_activate" -alias exceptions.deactivate="exceptions_deactivate" -alias exceptions.try='exceptions_enter_try; (exceptions_activate; ' -alias exceptions.catch='true); exceptions_exit_try $? || ' diff --git a/logging.sh b/logging.sh deleted file mode 100644 index 9badd6b..0000000 --- a/logging.sh +++ /dev/null @@ -1,481 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source "$(dirname "${BASH_SOURCE[0]}")/core.sh" - -core.import ui -core.import array -core.import arguments -logging__doc__=' - The available log levels are: - error critical warn info debug - - The standard loglevel is critical - >>> logging.get_level - >>> logging.get_commands_level - critical - critical - >>> logging.error error-message - >>> logging.critical critical-message - >>> logging.warn warn-message - >>> logging.info info-message - >>> logging.debug debug-message - +doc_test_contains - error-message - critical-message - - If the output of commands should be printed, the commands_level needs to be - greater than or equal to the log_level. - >>> logging.set_level critical - >>> logging.set_commands_level debug - >>> echo foo - - >>> logging.set_level info - >>> logging.set_commands_level info - >>> echo foo - foo - - Another logging prefix can be set by overriding "logging_get_prefix". - >>> logging_get_prefix() { - >>> local level=$1 - >>> echo "[myprefix - ${level}]" - >>> } - >>> logging.critical foo - [myprefix - critical] foo - - "logging.plain" can be used to print at any log level and without the - prefix. - >>> logging.set_level critical - >>> logging.set_commands_level debug - >>> logging.plain foo - foo - - "logging.cat" can be used to print files (e.g "logging.cat < file.txt") - or heredocs. Like "logging.plain", it also prints at any log level and - without the prefix. - >>> echo foo | logging.cat - foo -' - -# region variables -# logging levels from low to high -logging_levels=(error critical warn info verbose debug) -# matches the order of logging levels -logging_levels_color=( - $ui_color_red - $ui_color_magenta - $ui_color_yellow - $ui_color_cyan - $ui_color_green - $ui_color_blue -) -logging_commands_level=$(array.get_index 'critical' "${logging_levels[@]}") -logging_level=$(array.get_index 'critical' "${logging_levels[@]}") -# endregion -# region functions -logging_set_commands_level() { - logging_commands_level=$(array.get_index "$1" "${logging_levels[@]}") - if [ "$logging_level" -ge "$logging_commands_level" ]; then - logging_set_command_output_on - else - logging_set_command_output_off - fi -} -logging_get_level() { - echo "${logging_levels[$logging_level]}" -} -logging_get_commands_level() { - echo "${logging_levels[$logging_commands_level]}" -} -logging_set_level() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - >>> logging.set_commands_level info - >>> logging.set_level info - >>> echo $logging_level - >>> echo $logging_commands_level - 3 - 3 - ' - logging_level=$(array.get_index "$1" "${logging_levels[@]}") - if [ "$logging_level" -ge "$logging_commands_level" ]; then - logging_set_command_output_on - else - logging_set_command_output_off - fi -} -logging_get_prefix() { - local level=$1 - local level_index=$2 - local color=${logging_levels_color[$level_index]} - # shellcheck disable=SC2154 - local loglevel=${color}${level}${ui_color_default} - local path="${BASH_SOURCE[2]##./}" - path=$(basename "$path") - local prefix=[${loglevel}:"$path":${BASH_LINENO[1]}] - echo "$prefix" -} -logging_log() { - local level="$1" - shift - local level_index - level_index=$(array.get_index "$level" "${logging_levels[@]}") - if [ "$level_index" -eq -1 ]; then - logging_log critical "loglevel \"$level\" not available, use one of: "\ - "${logging_levels[@]}" - return 1 - fi - if [ "$logging_level" -ge "$level_index" ]; then - logging_plain "$(logging_get_prefix "$level" "$level_index")" "$@" - fi -} -logging_output_to_saved_file_descriptors=false -logging_off=false -logging_cat() { - $logging_off && return 0 - if [[ "$logging_log_file" != "" ]]; then - cat "$@" >> "$logging_log_file" - if $logging_tee_fifo_active; then - cat "$@" - fi - else - if $logging_output_to_saved_file_descriptors; then - cat "$@" 1>&3 2>&4 - else - cat "$@" - fi - fi -} -logging_plain() { - local __doc__=' - >>> logging.set_level info - >>> logging.set_commands_level debug - >>> logging.debug "not shown" - >>> echo "not shown" - >>> logging.plain "shown" - shown - - ' - $logging_off && return 0 - if [[ "$logging_log_file" != "" ]]; then - echo -e "$@" >> "$logging_log_file" - if $logging_tee_fifo_active; then - echo -e "$@" - fi - else - if $logging_output_to_saved_file_descriptors; then - echo -e "$@" 1>&3 2>&4 - else - echo -e "$@" - fi - fi -} -logging_commands_output_saved="std" -logging_set_command_output_off() { - logging_commands_output_saved="$logging_options_command" - logging_set_file_descriptors "$logging_log_file" \ - --logging="$logging_options_log" --commands="off" -} -logging_set_command_output_on() { - logging_set_file_descriptors "$logging_log_file" \ - --logging="$logging_options_log" \ - --commands="std" -} -logging_log_file='' -# shellcheck disable=SC2034 -logging_tee_fifo="" -logging_tee_fifo_dir="" -logging_tee_fifo_active=false -logging_file_descriptors_saved=false -logging_commands_tee_fifo_active=false -logging_options_log="std" -logging_options_command="std" -logging_set_log_file() { - local __doc__=' - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging.set_log_file "$test_file" - >>> logging.plain logging - >>> logging.set_log_file "$test_file" - >>> echo echo - >>> logging.set_log_file "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - logging - echo - test_file: - logging - echo - - >>> logging.set_commands_level debug - >>> logging.set_level debug - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging.set_log_file "$test_file" - >>> logging.plain 1 - >>> logging.set_log_file "" - >>> logging.set_log_file "$test_file" - >>> logging.plain 2 - >>> logging.set_log_file "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - 1 - 2 - test_file: - 1 - 2 - ' - [[ "$logging_log_file" == "$1" ]] && return 0 - logging_set_file_descriptors "" - [[ "$1" == "" ]] && return 0 - logging_set_file_descriptors "$1" --commands=tee --logging=tee -} -logging_set_file_descriptors() { - local __doc__=' - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - test_file: - - >>> local test_file="$(mktemp)" - >>> logging_set_file_descriptors "$test_file" - >>> logging_set_file_descriptors "" - >>> echo "test_file:" >"$test_file" - >>> logging.cat "$test_file" - >>> rm "$test_file" - test_file: - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=tee - >>> logging.plain foo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - foo - test_file: - foo - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=off --commands=file - >>> logging.plain not shown - >>> echo foo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - test_file: - foo - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=off - >>> logging.plain not shown - >>> echo foo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - foo - test_file: - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --commands=tee - >>> logging.plain logging - >>> echo echo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - logging - echo - test_file: - echo - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --commands=file - >>> logging.plain logging - >>> echo echo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - logging - test_file: - echo - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=file --commands=file - >>> logging.plain logging - >>> echo echo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - test_file: - logging - echo - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=file --commands=file - >>> logging.plain logging - >>> echo echo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - test_file: - logging - echo - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=file --commands=tee - >>> logging.plain logging - >>> echo echo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - echo - test_file: - logging - echo - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=file --commands=off - >>> logging.plain logging - >>> echo echo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - test_file: - logging - - >>> local test_file="$(mktemp)" - >>> logging.plain "test_file:" >"$test_file" - >>> logging_set_file_descriptors "$test_file" --logging=tee --commands=tee - >>> logging.plain logging - >>> echo echo - >>> logging_set_file_descriptors "" - >>> logging.cat "$test_file" - >>> rm "$test_file" - logging - echo - test_file: - logging - echo - - Test exit handler - >>> local test_file fifo - >>> test_file="$(mktemp)" - >>> fifo=$(logging_set_file_descriptors "$test_file" --commands=tee; \ - >>> echo $logging_tee_fifo) - >>> [ -p "$fifo" ] || echo fifo deleted - >>> rm "$test_file" - fifo deleted - ' - arguments.set "$@" - # one off "std off tee file" - local options_log options_command - arguments.get_keyword --logging options_log - arguments.get_keyword --commands options_command - [[ "${options_log-}" == "" ]] && options_log=std - [[ "${options_command-}" == "" ]] && options_command=std - logging_options_log="$options_log" - logging_options_command="$options_command" - set -- "${arguments_new_arguments[@]:-}" - local log_file="$1" - - logging_off=false - # restore - if $logging_file_descriptors_saved; then - exec 1>&3 2>&4 3>&- 4>&- - logging_file_descriptors_saved=false - fi - [ -p "$logging_tee_fifo" ] && rm -rf "$logging_tee_fifo_dir" - logging_commands_tee_fifo_active=false - logging_tee_fifo_active=false - logging_output_to_saved_file_descriptors=false - - if [[ "$log_file" == "" ]]; then - logging_log_file="" - [[ "$logging_options_log" == "tee" ]] && return 1 - [[ "$logging_options_command" == "tee" ]] && return 1 - if [[ "$logging_options_log" == "off" ]]; then - logging_off=true - fi - if [[ "$logging_options_command" == "off" ]]; then - exec 3>&1 4>&2 - logging_file_descriptors_saved=true - exec &>/dev/null - logging_output_to_saved_file_descriptors=true - fi - return 0 - fi - # It's guaranteed that we have a log file from here on. - - if ! $logging_file_descriptors_saved; then - # save /dev/stdout and /dev/stderr to &3, &4 - exec 3>&1 4>&2 - logging_file_descriptors_saved=true - fi - - if [[ "$logging_options_log" == tee ]]; then - if [[ "$logging_options_command" != "tee" ]]; then - logging_log_file="$log_file" - logging_tee_fifo_active=true - fi - elif [[ "$logging_options_log" == "stdout" ]]; then - true - elif [[ "$logging_options_log" == "file" ]]; then - logging_log_file="$log_file" - elif [[ "$logging_options_log" == "off" ]]; then - logging_off=true - fi - if [[ "$logging_options_command" == "tee" ]]; then - logging_tee_fifo_dir="$(mktemp --directory --suffix rebash-logging-fifo)" - logging_tee_fifo="$logging_tee_fifo_dir/fifo" - mkfifo "$logging_tee_fifo" - trap '[ -p "$logging_tee_fifo" ] && rm -rf "$logging_tee_fifo_dir"; exit' EXIT - tee --append "$log_file" <"$logging_tee_fifo" & - exec 1>>"$logging_tee_fifo" 2>>"$logging_tee_fifo" - logging_commands_tee_fifo_active=true - if [[ "$logging_options_log" != tee ]]; then - logging_output_to_saved_file_descriptors=true - fi - elif [[ "$logging_options_command" == "stdout" ]]; then - true - elif [[ "$logging_options_command" == "file" ]]; then - exec 1>>"$log_file" 2>>"$log_file" - logging_output_to_saved_file_descriptors=true - elif [[ "$logging_options_command" == "off" ]]; then - exec 1>>/dev/null 2>>/dev/null - fi -} - -# endregion -# region public interface -alias logging.set_level='logging_set_level' -alias logging.set_commands_level='logging_set_commands_level' -alias logging.get_level='logging_get_level' -alias logging.get_commands_level='logging_get_commands_level' -alias logging.log='logging_log' -alias logging.error='logging_log error' -alias logging.critical='logging_log critical' -alias logging.warn='logging_log warn' -alias logging.info='logging_log info' -alias logging.verbose='logging_log verbose' -alias logging.debug='logging_log debug' -alias logging.plain='logging_plain' -alias logging.cat='logging_cat' -alias logging.set_file_descriptors='logging_set_file_descriptors' -alias logging.set_log_file='logging_set_log_file' -# endregion -# region vim modline - -# vim: set tabstop=4 shiftwidth=4 expandtab: -# vim: foldmethod=marker foldmarker=region,endregion: - -# endregion diff --git a/time.sh b/time.sh deleted file mode 100644 index a370074..0000000 --- a/time.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source $(dirname ${BASH_SOURCE[0]})/core.sh - -time_timer_start_time="" -time_timer_start() { - time_timer_start_time=$(date +%s%N) -} -time_timer_get_elapsed() { - local end_time="$(date +%s%N)" - local elapsed_time_in_ns=$(( $end_time - $time_timer_start_time )) - local elapsed_time_in_ms=$(( $elapsed_time_in_ns / 1000000 )) - echo "$elapsed_time_in_ms" -} -alias time.timer_start="time_timer_start" -alias time.timer_get_elapsed="time_timer_get_elapsed" diff --git a/ui.sh b/ui.sh deleted file mode 100644 index 6fa69a8..0000000 --- a/ui.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source $(dirname ${BASH_SOURCE[0]})/core.sh -# shellcheck disable=SC2034 -ui__doc__=' - This module provides variables for printing colorful and unicode glyphs. - The Terminal features are detected automatically but can also be - enabled/disabled manually (see - [ui.enable_color](#function-ui_enable_color) and - [ui.enable_unicode_glyphs](#function-ui_enable_unicode_glyphs)). -' -# region colors -ui_color_enabled=false -ui_enable_color() { - local __doc__=' - Enables color output explicitly. - - >>> ui.disable_color - >>> ui.enable_color - >>> echo -E $ui_color_red red $ui_color_default - \033[0;31m red \033[0m - ' - ui_color_enabled=true - ui_color_default='\033[0m' - - ui_color_black='\033[0;30m' - ui_color_red='\033[0;31m' - ui_color_green='\033[0;32m' - ui_color_yellow='\033[0;33m' - ui_color_blue='\033[0;34m' - ui_color_magenta='\033[0;35m' - ui_color_cyan='\033[0;36m' - ui_color_lightgray='\033[0;37m' - - ui_color_darkgray='\033[0;90m' - ui_color_lightred='\033[0;91m' - ui_color_lightgreen='\033[0;92m' - ui_color_lightyellow='\033[0;93m' - ui_color_lightblue='\033[0;94m' - ui_color_lightmagenta='\033[0;95m' - ui_color_lightcyan='\033[0;96m' - ui_color_white='\033[0;97m' - - # flags - ui_color_bold='\033[1m' - ui_color_dim='\033[2m' - ui_color_underline='\033[4m' - ui_color_blink='\033[5m' - ui_color_invert='\033[7m' - ui_color_invisible='\033[8m' - - ui_color_nobold='\033[21m' - ui_color_nodim='\033[22m' - ui_color_nounderline='\033[24m' - ui_color_noblink='\033[25m' - ui_color_noinvert='\033[27m' - ui_color_noinvisible='\033[28m' -} - -# shellcheck disable=SC2034 -ui_disable_color() { - local __doc__=' - Disables color output explicitly. - - >>> ui.enable_color - >>> ui.disable_color - >>> echo -E "$ui_color_red" red "$ui_color_default" - red - ' - ui_color_enabled=false - ui_color_default='' - - ui_color_black='' - ui_color_red='' - ui_color_green='' - ui_color_yellow='' - ui_color_blue='' - ui_color_magenta='' - ui_color_cyan='' - ui_color_lightgray='' - - ui_color_darkgray='' - ui_color_lightred='' - ui_color_lightgreen='' - ui_color_lightyellow='' - ui_color_lightblue='' - ui_color_lightmagenta='' - ui_color_lightcyan='' - ui_color_white='' - - # flags - ui_color_bold='' - ui_color_dim='' - ui_color_underline='' - ui_color_blink='' - ui_color_invert='' - ui_color_invisible='' - - ui_color_nobold='' - ui_color_nodim='' - ui_color_nounderline='' - ui_color_noblink='' - ui_color_noinvert='' - ui_color_noinvisible='' -} -# endregion -# region glyphs -# NOTE: use 'xfd -fa ' to watch glyphs -ui_unicode_enabled=false -ui_enable_unicode_glyphs() { - local __doc__=' - Enables unicode glyphs explicitly. - - >>> ui.disable_unicode_glyphs - >>> ui.enable_unicode_glyphs - >>> echo -E "$ui_powerline_ok" - \u2714 - ' - ui_unicode_enabled=true - ui_powerline_pointingarrow='\u27a1' - ui_powerline_arrowleft='\ue0b2' - ui_powerline_arrowright='\ue0b0' - ui_powerline_arrowrightdown='\u2198' - ui_powerline_arrowdown='\u2b07' - ui_powerline_plusminus='\ue00b1' - ui_powerline_branch='\ue0a0' - ui_powerline_refersto='\u27a6' - ui_powerline_ok='\u2714' - ui_powerline_fail='\u2718' - ui_powerline_lightning='\u26a1' - ui_powerline_cog='\u2699' - ui_powerline_heart='\u2764' - - # colorful - ui_powerline_star='\u2b50' - ui_powerline_saxophone='\u1f3b7' - ui_powerline_thumbsup='\u1f44d' -} - -# shellcheck disable=SC2034 -ui_disable_unicode_glyphs() { - local __doc__=' - Disables unicode glyphs explicitly. - - >>> ui.enable_unicode_glyphs - >>> ui.disable_unicode_glyphs - >>> echo -E "$ui_powerline_ok" - + - ' - ui_unicode_enabled=false - ui_powerline_pointingarrow='~' - ui_powerline_arrowleft='<' - ui_powerline_arrowright='>' - ui_powerline_arrowrightdown='>' - ui_powerline_arrowdown='_' - ui_powerline_plusminus='+-' - ui_powerline_branch='|}' - ui_powerline_refersto='*' - ui_powerline_ok='+' - ui_powerline_fail='x' - ui_powerline_lightning='!' - ui_powerline_cog='{*}' - ui_powerline_heart='<3' - - # colorful - ui_powerline_star='*' - ui_powerline_saxophone='(yeah)' - ui_powerline_thumbsup='(ok)' -} -# endregion -# region detect terminal capabilities -if [[ "${TERM}" == *"xterm"* ]]; then - ui_enable_color -else - ui_disable_color -fi - -# TODO improve unicode detection -ui_glyph_available_in_font() { - - #local font=$1 - local current_font - current_font=$(xrdb -q| grep -i facename | cut -d: -f2) - local font_file_name - font_file_name=$(fc-match "$current_font" | cut -d: -f1) - #font_path=$(fc-list "$current_font" | grep "$font_file_name" | cut -d: -f1) - local font_file_extension="${font_file_name##*.}" - - # Alternative or to be sure - #font_path=$(lsof -p $(ps -o ppid= -p $$) | grep fonts) - - if [[ $font_file_extension == otf ]]; then - otfinfo /usr/share/fonts/OTF/Hack-Regular.otf -u | grep -i uni27a1 - elif [[ $font_file_extension == ttf ]]; then - ttfdump -t cmap /usr/share/fonts/TTF/Hack-Regular.ttf 2>/dev/null| grep 'Char 0x27a1' - else - return 1 - fi - return $? -} -# TODO this breaks dracut (segfault) -#(echo -e $'\u1F3B7' | grep -v F3B7) &> /dev/null -if core_is_defined NO_UNICODE; then - ui_disable_unicode_glyphs -else - ui_enable_unicode_glyphs -fi -# endregion -# region public interface -alias ui.enable_color='ui_enable_color' -alias ui.disable_color='ui_disable_color' -alias ui.enable_unicode_glyphs='ui_enable_unicode_glyphs' -alias ui.disable_unicode_glyphs='ui_disable_unicode_glyphs' -# endregion -# region vim modline - -# vim: set tabstop=4 shiftwidth=4 expandtab: -# vim: foldmethod=marker foldmarker=region,endregion: - -# endregion diff --git a/utils.sh b/utils.sh deleted file mode 100644 index cb116f2..0000000 --- a/utils.sh +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env bash -# shellcheck source=./core.sh -source "$(dirname "${BASH_SOURCE[0]}")/core.sh" -core.import logging - -utils_dependency_check_pkgconfig() { - local __doc__=' - This function check if all given libraries can be found. - - #### Example: - - >>> utils_dependency_check_shared_library libc.so; echo $? - 0 - >>> utils_dependency_check_shared_library libc.so __not_existing__ 1>/dev/null; echo $? - 2 - >>> utils_dependency_check_shared_library __not_existing__ 1>/dev/null; echo $? - 2 - ' - local return_code=0 - local library - - if ! utils_dependency_check pkg-config &>/dev/null; then - logging.critical 'Missing dependency "pkg-config" to check for packages.' - return 1 - fi - for library in $@; do - if ! pkg-config "$library" &>/dev/null; then - return_code=2 - echo "$library" - fi - done - return $return_code -} -utils_dependency_check_shared_library() { - local __doc__=' - This function check if all given shared libraries can be found. - - #### Example: - - >>> utils_dependency_check_shared_library libc.so; echo $? - 0 - >>> utils_dependency_check_shared_library libc.so __not_existing__ 1>/dev/null; echo $? - 2 - >>> utils_dependency_check_shared_library __not_existing__ 1>/dev/null; echo $? - 2 - ' - local return_code=0 - local pattern - - if ! utils_dependency_check ldconfig &>/dev/null; then - logging.critical 'Missing dependency "ldconfig" to check for shared libraries.' - return 1 - fi - for pattern in $@; do - if ! ldconfig --print-cache | cut --fields 1 --delimiter ' ' | \ - grep "$pattern" &>/dev/null - then - return_code=2 - echo "$pattern" - fi - done - return $return_code -} -utils_dependency_check() { - # shellcheck disable=SC2034 - local __doc__=' - This function check if all given dependencies are present. - - #### Example: - - >>> utils_dependency_check mkdir ls; echo $? - 0 - >>> utils_dependency_check mkdir __not_existing__ 1>/dev/null; echo $? - 2 - >>> utils_dependency_check __not_existing__ 1>/dev/null; echo $? - 2 - >>> utils_dependency_check "ls __not_existing__"; echo $? - __not_existing__ - 2 - ' - local return_code=0 - local dependency - - if ! hash &>/dev/null; then - logging.critical 'Missing dependency "hash" to check for available executables.' - return 1 - fi - for dependency in $@; do - if ! hash "$dependency" &>/dev/null; then - return_code=2 - echo "$dependency" - fi - done - return $return_code -} -utils__doc_test_setup__=' -lsblk() { - if [[ "${@: -1}" == "" ]];then - echo "lsblk: : not a block device" - return 1 - fi - if [[ "${@: -1}" != "/dev/sdb" ]];then - echo "/dev/sda disk" - echo "/dev/sda1 part SYSTEM_LABEL 0x7" - echo "/dev/sda2 part" - fi - if [[ "${@: -1}" != "/dev/sda" ]];then - echo "/dev/sdb disk" - echo "/dev/sdb1 part boot_partition " - echo "/dev/sdb2 part system_partition" - fi -} -blkid() { - [[ "${@: -1}" != "/dev/sda2" ]] && return 0 - echo "gpt" - echo "only discoverable by blkid" - echo "boot_partition" - echo "192d8b9e" -} -' -utils_find_block_device() { - # shellcheck disable=SC2034,SC2016 - local __doc__=' - >>> utils_find_block_device "boot_partition" - /dev/sdb1 - >>> utils_find_block_device "boot_partition" /dev/sda - /dev/sda2 - >>> utils_find_block_device "discoverable by blkid" - /dev/sda2 - >>> utils_find_block_device "_partition" - /dev/sdb1 /dev/sdb2 - >>> utils_find_block_device "not matching anything" || echo not found - not found - >>> utils_find_block_device "" || echo not found - not found - ' - local partition_pattern="$1" - local device="${2-}" - - [ "$partition_pattern" = "" ] && return 1 - utils_find_block_device_simple() { - local device_info - lsblk --noheadings --list --paths --output \ - NAME,TYPE,LABEL,PARTLABEL,UUID,PARTUUID ${device:+"$device"} \ - | sort --unique | while read -r device_info; do - local current_device - current_device=$(echo "$device_info" | cut -d' ' -f1) - if [[ "$device_info" = *"${partition_pattern}"* ]]; then - echo "$current_device" - fi - done - } - utils_find_block_device_deep() { - local device_info - lsblk --noheadings --list --paths --output NAME ${device:+"$device"} \ - | sort --unique | cut -d' ' -f1 | while read -r current_device; do - blkid -p -o value "$current_device" \ - | while read -r device_info; do - if [[ "$device_info" = *"${partition_pattern}"* ]]; then - echo "$current_device" - fi - done - done - } - local candidates - candidates=($(utils_find_block_device_simple)) - [ ${#candidates[@]} -eq 0 ] && candidates=($(utils_find_block_device_deep)) - unset -f utils_find_block_device_simple - unset -f utils_find_block_device_deep - [ ${#candidates[@]} -eq 0 ] && return 1 - [ ${#candidates[@]} -ne 1 ] && echo "${candidates[@]}" && return 1 - logging.plain "${candidates[0]}" -} -utils_create_partition_via_offset() { - local device="$1" - local nameOrUUID="$2" - local loop_device - loop_device="$(losetup --find)" - local sector_size - sector_size="$(blockdev --getbsz "$device")" - - # NOTE: partx's NAME field corresponds to partition labels - local partitionInfo - partitionInfo=$(partx --raw --noheadings --output \ - START,NAME,UUID,TYPE "$device" 2>/dev/null| grep "$nameOrUUID") - local offsetSectors - offsetSectors="$(echo "$partitionInfo"| cut --delimiter ' ' \ - --fields 1)" - if [ -z "$offsetSectors" ]; then - logging.warn "Could not find partition with label/uuid \"$nameOrUUID\" on device \"$device\"" - return 1 - fi - local offsetBytes - offsetBytes="$(echo | awk -v x="$offsetSectors" -v y="$sector_size" '{print x * y}')" - losetup --offset "$offsetBytes" "$loop_device" "$device" - logging.plain "$loop_device" -} -utils_random_string() { - local length="$1" - cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c "$length" -} -alias utils.dependency_check_pkgconfig="utils_dependency_check_pkgconfig" -alias utils.dependency_check_shared_library="utils_dependency_check_shared_library" -alias utils.dependency_check="utils_dependency_check" -alias utils.find_block_device="utils_find_block_device" -alias utils.create_partition_via_offset="utils_create_partition_via_offset" -alias utils.random_string="utils_random_string" From 0dc4b9d2660c9ee10f5c351985803f11e2032363 Mon Sep 17 00:00:00 2001 From: Devon Bagley Date: Wed, 27 Oct 2021 17:46:15 -0700 Subject: [PATCH 6/7] include post-install post-remove scripts for packages --- after-install.sh | 5 +++++ after-remove.sh | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 after-install.sh create mode 100644 after-remove.sh diff --git a/after-install.sh b/after-install.sh new file mode 100644 index 0000000..4fa69c6 --- /dev/null +++ b/after-install.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +echo "export REBASH_HOME=/usr/lib/rebash" | tee /etc/.rebash +chmod 0755 /etc/.rebash +ln -s /usr/bin/rebash /usr/lib/rebash.sh diff --git a/after-remove.sh b/after-remove.sh new file mode 100644 index 0000000..122cdf7 --- /dev/null +++ b/after-remove.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +rm -rf /etc/.rebash +rm -f /usr/lib/rebash.sh From a1cd9367e8ff0fcf5c19101f4f0f00806c4bce17 Mon Sep 17 00:00:00 2001 From: Devon Bagley Date: Wed, 27 Oct 2021 19:31:40 -0700 Subject: [PATCH 7/7] Add docker repl --- Dockerfile | 9 +++++++++ Makefile | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ec72d36 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +from alpine:latest + +COPY dist/rebash-0.0.8-any.apk /tmp/ + +RUN apk add --allow-untrusted /tmp/rebash-0.0.8-any.apk + +ENTRYPOINT [ "/bin/bash", "-c" ] + +CMD [ "exec bash --init-file <(echo '. /usr/lib/rebash.sh') -i" ] diff --git a/Makefile b/Makefile index 0ed5af1..6aec195 100644 --- a/Makefile +++ b/Makefile @@ -20,4 +20,11 @@ package: # fpm -t pacman -p dist/rebash-$(VERSION)-any.pkg bsdtar required? fpm -t deb -p dist/rebash-$(VERSION)-any.deb fpm -t rpm -p dist/rebash-$(VERSION)-any.rpm - fpm -t apk -p dist/rebash-$(VERSION)-any.apk + fpm -t apk --depends coreutils -p dist/rebash-$(VERSION)-any.apk + +docker: + make package + docker build -t jandob/rebash:0.0.8 . + docker push jandob/rebash:0.0.8 && \ + docker tag jandob/rebash:0.0.8 jandob/rebash:latest && \ + docker push jandob/rebash:latest