From 6dc6b305b90133556dc73e70ac12673be49aace7 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Fri, 18 Mar 2022 17:17:28 +0100 Subject: [PATCH 01/18] Initial file structrue and rdlink with essential content --- .editorconfig | 6 +++ rdlink.sh | 112 ++++++++++++++++++++++++++++++++++++++++++++ test/test_rdlink.sh | 72 ++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 .editorconfig create mode 100755 rdlink.sh create mode 100755 test/test_rdlink.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..270aad7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*.sh] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/rdlink.sh b/rdlink.sh new file mode 100755 index 0000000..b4b6a58 --- /dev/null +++ b/rdlink.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +## rdlink [-d|--debug] +## +## License: GNU GPL V3 or later +## Author: Martin Winkler + +# Exit on error. Append "|| true" if you expect an error. +set -o errexit +# Exit on error inside any functions or subshells. +set -o errtrace +# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR +set -o nounset +# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip` +set -o pipefail + +LOG_LEVEL="${LOG_LEVEL:-1}" # 7 = debug -> 0 = emergency + +rl::rdlink() { + local normalized= + info "Processing... $@" + normalized="$(rl::normalize "${1:-}")" + rl::quicklink "${normalized}" +} + +rl::canon() { + info "Canonicalize path... $@" +} + +rl::quicklink() { + info "Quick rdlink... ${1:-}" + readlink "${1:-}" || printf "${1:-}\n" +} + +rl::normalize() { + info "Normalizing... ${1:-}" + printf "${1:-}\n" +} + +rl::main() { + local file= + local arg= + + for arg in "$@"; do + case "$arg" in + --) + shift + break ;; + -d|--debug) + shift + LOG_LEVEL=7 ;; + -dd) + shift + set -o xtrace ;; + -*) + shift ;; + esac + done + + info "BASH rdlink" + + for file in "$@"; do + rl::rdlink "$file" + done +} + +function rl::log () { + local log_level="${1:-${LOG_LEVEL}}" + shift + + # all remaining arguments are to be printed + local log_line="" + + while IFS=$'\n' read -r log_line; do + printf "%s [%9s] %s\n" "$(date -u +"%Y-%m-%d %H:%M:%S UTC")" "${log_level}" "${log_line}" 1>&2 + done <<< "${@:-}" +} +emergency () { rl::log emergency "${@}"; exit 1; } +alarm() { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && rl::log alert "${@}"; true; } +critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && rl::log critical "${@}"; true; } +error () { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && rl::log error "${@}"; true; } +warning () { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && rl::log warning "${@}"; true; } +notice () { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && rl::log notice "${@}"; true; } +info () { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && rl::log info "${@}"; true; } +debug () { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && rl::log debug "${@}"; true; } +msg () { echo "$@"; true; } + + +# Provide as function to be called when sourced +rdlink() { + rl::main "$@" +} + +### Check if script is _sourced +### https://stackoverflow.com/a/28776166 +_sourced=0 +if [ -n "${ZSH_EVAL_CONTEXT:-}" ]; then + case ${ZSH_EVAL_CONTEXT:-} in *:file) _sourced=1;; esac +elif [ -n "${KSH_VERSION:-}" ]; then + [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && _sourced=1 +elif [ -n "${BASH_VERSION:-}" ]; then + (return 0 2>/dev/null) && _sourced=1 +else # All other shells: examine $0 for known shell binary filenames + # Detects `sh` and `dash`; add additional shell filenames as needed. + case ${0##*/} in sh|dash) _sourced=1;; esac +fi +### + +if (( ! _sourced )); then + rl::main "$@" +fi + diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh new file mode 100755 index 0000000..5944e13 --- /dev/null +++ b/test/test_rdlink.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +tool_a="/opt/rdlink/rdlink.sh" +tool_b="$(command -v readlink) -f" + +test_links=( "Valid links" + "/dev/stdin" + "$HOME/tmp/a"* + "$HOME/tmp/a 3" + "/bin/adb" +) + +test_all=( "Test everything starting from /" + /**/* +) + +test_args=( "Tests from command line" + "$@" +) + +totest=( + test_links +) + +[[ "$@" ]] && totest=(test_args) + +rl::test() { + local i=0 + local ra= + local rb= + local path= + local testarray= + local testend=0 + + for testarray in "${totest[@]}"; do + i=0 + testarray="$testarray"[@] + for path in "${!testarray}"; do + + if (( ! i )); then + # Print title in array element 0 + printf " ### %s ###\n" "$path" + else + # Execute tests + printf -- "%-3d --- %-30s ---" $i "$path" + ra="$(${tool_a} "$path")" + rb="$(${tool_b} "$path")" + if [[ "$ra" = "$rb" ]]; then + # Test successful + printf " OK\n" + else + # Test failed + printf " FAIL\n" + printf "\n Result:\n%s\n%s\n" "$ra" "$rb" + printf "\n Subject:\n" + ls -al "$path" + + # Print debug output of tool_a + printf "\n Debug:\n" + ( "${tool_a}" -d "${path}" ) + + testend=1 && break + fi + fi + + ((i++)) + done + (( testend )) && break + done +} + +time rl::test -- 2.30.2 From e878a7b2ac9451acc95e47032642c38757c55108 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Fri, 18 Mar 2022 19:09:58 +0100 Subject: [PATCH 02/18] Refactor to support more tests Extend testcases: assertion test and competition tests --- test/test_rdlink.sh | 146 +++++++++++++++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 37 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index 5944e13..226b515 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -1,71 +1,143 @@ -#!/bin/bash +#!/usr/bin/env bash -tool_a="/opt/rdlink/rdlink.sh" -tool_b="$(command -v readlink) -f" +# Automatic tests for rdlink +# Two kinds of tests are supported +# * `tocompete` : competition between readlink and rdlink +# * `toassert` : assert expected path and input path -test_links=( "Valid links" +# test_rdlink.sh [PATH TO COMPETE] +# +# Internal tests will be executed if no arguments are found + +test_dir="/opt/rdlink/test" +tool_rdlink="/opt/rdlink/rdlink.sh" +tool_readlink="$(command -v readlink) -f" + +########## Competition Block ########## +# Each competition link is given to both tools +# and the output is compared +# [0] = Title +# [1] = path for competition +# [2] = path for competition +# ... +compete_links=( "Valid links" "/dev/stdin" - "$HOME/tmp/a"* - "$HOME/tmp/a 3" + "$test_dir/tmp/a"* + "$test_dir/tmp/a 3" "/bin/adb" ) -test_all=( "Test everything starting from /" +compete_all=( "Test everything starting from /" /**/* ) -test_args=( "Tests from command line" +compete_args=( "Tests from command line" "$@" ) -totest=( - test_links +# Lists for competition test between rdlink and readlink -f +tocompete=( + compete_links ) -[[ "$@" ]] && totest=(test_args) +########## Assertion Block ########## +# Assertion string compare test +# [0] = Title +# [1] expected == [2] input +# [3] expected == [4] input +# ... +assert_string=( "Assert invalid files" + "/opt/rdlink/test/a" "a" + "/opt/rdlink/test/b" "b" + "/opt/rdlink/test/c" "c" +) + +# List of assertian test arrays +toassert=( + assert_string +) + +# Only run `compete_args` if arguments are available +[[ "$@" ]] && tocompete=(compete_args) && toassert=() + +# rl::testcmp +rl::testcmp() { + local testnum= + local expect= + local actual= + local input= + printf -- "\n%-3d --- %-30s ---" ${testnum:=${1:-0}} "${input:=${2:-"-"}}" + if [[ "${expect:=${3:-"-"}}" == "${actual:=${4:-"-"}}" ]]; then + # Test successful + printf " OK\n" + else + # Test failed + printf " FAIL\n" + printf " Result:\nExp: %s\nAct: %s\n" "${expect}" "${actual}" + [ -e "${input}" ] && printf "\n Subject:\n" && ls -al "${input}" + + # Print debug output of tool_a + printf "\n Debug:\n" + ( "${tool_rdlink}" -d "${input}" ) + return 1 + fi + return 0 +} rl::test() { local i=0 - local ra= - local rb= local path= - local testarray= + local arraywalker= + local firstelement= local testend=0 - for testarray in "${totest[@]}"; do + # Compare against expected result + for testarray in "${toassert[@]}"; do i=0 - testarray="$testarray"[@] - for path in "${!testarray}"; do - + arraywalker="$testarray"[@] + for path in "${!arraywalker}"; do + if (( ! i )); then + # Print title in array element 0 + printf " ### %s ###\n" "$path" + elif [ -z "$firstelement" ]; then + # Save first element for string compare + firstelement="${path}" + else + # Do the compare between two following elements + if ! rl::testcmp "$((i-1))" "${path}" \ + "${firstelement}" "$("$tool_rdlink" "${path}")"; then + testend=1 + break + fi + + firstelement= + fi + ((i++)) + done + (( testend )) && return 1 + done + + # Compare output of rdlink and readlink -f + for testarray in "${tocompete[@]}"; do + i=0 + arraywalker="$testarray"[@] + for path in "${!arraywalker}"; do + if (( ! i )); then # Print title in array element 0 printf " ### %s ###\n" "$path" else # Execute tests - printf -- "%-3d --- %-30s ---" $i "$path" - ra="$(${tool_a} "$path")" - rb="$(${tool_b} "$path")" - if [[ "$ra" = "$rb" ]]; then - # Test successful - printf " OK\n" - else - # Test failed - printf " FAIL\n" - printf "\n Result:\n%s\n%s\n" "$ra" "$rb" - printf "\n Subject:\n" - ls -al "$path" - - # Print debug output of tool_a - printf "\n Debug:\n" - ( "${tool_a}" -d "${path}" ) - - testend=1 && break + if ! rl::testcmp "${i}" "${path}" \ + "$(${tool_readlink} "$path")" "$(${tool_rdlink} "$path")"; then + testend=1 + break fi fi ((i++)) done - (( testend )) && break + (( testend )) && return 1 done } -- 2.30.2 From 1802f18339395d74d0317fed53dff4f3d632a0a9 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Fri, 18 Mar 2022 21:48:39 +0100 Subject: [PATCH 03/18] Outsourced test definition to separate file Add cmd options --, --run-all --- test/test_rdlink.sh | 94 +++++++++++++++++++-------------------------- test/totest.sh | 70 +++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 55 deletions(-) create mode 100644 test/totest.sh diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index 226b515..1fb748d 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -5,62 +5,24 @@ # * `tocompete` : competition between readlink and rdlink # * `toassert` : assert expected path and input path -# test_rdlink.sh [PATH TO COMPETE] -# +# test_rdlink.sh [OPTIONS] [PATH TO COMPETE] +# [OPTIONS] +# -a, --run-all : Run all tests (failed and successful) +# # Internal tests will be executed if no arguments are found -test_dir="/opt/rdlink/test" -tool_rdlink="/opt/rdlink/rdlink.sh" -tool_readlink="$(command -v readlink) -f" +readonly test_dir="/opt/rdlink/test" +readonly tool_rdlink="/opt/rdlink/rdlink.sh" +readonly tool_readlink="$(command -v readlink) -f" +flag_runall=0 -########## Competition Block ########## -# Each competition link is given to both tools -# and the output is compared -# [0] = Title -# [1] = path for competition -# [2] = path for competition -# ... -compete_links=( "Valid links" - "/dev/stdin" - "$test_dir/tmp/a"* - "$test_dir/tmp/a 3" - "/bin/adb" -) +toassert=() +tocompete=() -compete_all=( "Test everything starting from /" - /**/* -) - -compete_args=( "Tests from command line" - "$@" -) - -# Lists for competition test between rdlink and readlink -f -tocompete=( - compete_links -) - -########## Assertion Block ########## -# Assertion string compare test -# [0] = Title -# [1] expected == [2] input -# [3] expected == [4] input -# ... -assert_string=( "Assert invalid files" - "/opt/rdlink/test/a" "a" - "/opt/rdlink/test/b" "b" - "/opt/rdlink/test/c" "c" -) - -# List of assertian test arrays -toassert=( - assert_string -) - -# Only run `compete_args` if arguments are available -[[ "$@" ]] && tocompete=(compete_args) && toassert=() +. totest.sh # rl::testcmp +# Compare results and print summary rl::testcmp() { local testnum= local expect= @@ -79,6 +41,7 @@ rl::testcmp() { # Print debug output of tool_a printf "\n Debug:\n" ( "${tool_rdlink}" -d "${input}" ) + return 1 fi return 0 @@ -90,6 +53,24 @@ rl::test() { local arraywalker= local firstelement= local testend=0 + local arg= + + for arg in "$@"; do + case "$arg" in + --) ## End of options + shift && break ;; + -a|--run-all) ## Run all even if tests fail + flag_runall=1 + shift ;; + esac + done + + # Only run `compete_args` if arguments are available + if [[ "$@" ]]; then + compete_args=( "Tests from command line" "$@" ) + (( flag_runall )) && tocompete+=(compete_args) || tocompete=(compete_args) + toassert=() + fi # Compare against expected result for testarray in "${toassert[@]}"; do @@ -99,15 +80,17 @@ rl::test() { if (( ! i )); then # Print title in array element 0 printf " ### %s ###\n" "$path" + elif [ -z "$firstelement" ]; then # Save first element for string compare firstelement="${path}" + else # Do the compare between two following elements if ! rl::testcmp "$((i-1))" "${path}" \ "${firstelement}" "$("$tool_rdlink" "${path}")"; then - testend=1 - break + # Run all tests if option -a is pressend + (( ! $flag_runall )) && testend=1 && break fi firstelement= @@ -126,12 +109,13 @@ rl::test() { if (( ! i )); then # Print title in array element 0 printf " ### %s ###\n" "$path" + else # Execute tests if ! rl::testcmp "${i}" "${path}" \ "$(${tool_readlink} "$path")" "$(${tool_rdlink} "$path")"; then - testend=1 - break + # Run all tests if option -a is pressend + (( ! $flag_runall )) && testend=1 && break fi fi @@ -141,4 +125,4 @@ rl::test() { done } -time rl::test +time rl::test "$@" diff --git a/test/totest.sh b/test/totest.sh new file mode 100644 index 0000000..0bef7b3 --- /dev/null +++ b/test/totest.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +########## Competition Block ########## +# Each competition link is given to both tools +# and the output is compared +# [0] = Title +# [1] = path for competition +# [2] = path for competition +# ... +compete_links=( "Test - Valid links" + "/dev/stdin" + "$test_dir/tmp/a"* + "$test_dir/tmp/a 3" + "/bin/adb" +) + +compete_all=( "Test - everything starting from /" + /**/* +) + +tocompete_init() { + # TODO initialize custom test structure + + # Add tests to global test array from test_rdlink + tocompete+=( + compete_links + ) +} + +tocompete_clean() { + # TODO clean custom test structure + echo "tocompete_clean" +} + +tocompete_init + +########## Assertion Block ########## +# Assertion string compare test +# [0] = Title +# [1] expected == [2] input +# [3] expected == [4] input +# ... +assert_string=( "Assert - invalid files" + "/opt/rdlink/test/a" "a" + "/opt/rdlink/test/b" "b" + "/opt/rdlink/test/c" "c" +) + +toassert_init() { + # TODO initilaize custom test structure + + # Add test arrays to global test array from test_rdlink + toassert+=( + assert_string + ) +} + +toassert_clean() { + # TODO clean + echo "toassert_clean" +} + +totest_cleanall() { + echo + tocompete_clean + toassert_clean +} +trap totest_cleanall EXIT + +toassert_init -- 2.30.2 From c50bdefba57279bb4e68fe225c9f26bac07b89e7 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Sat, 19 Mar 2022 19:29:00 +0100 Subject: [PATCH 04/18] Progress on tests definition, init. and clean Update test result output mainly -o and integrated -oo into it --- test/test_rdlink.sh | 89 +++++++++++++++++++++++++++++++------------ test/totest.sh | 92 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 138 insertions(+), 43 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index 1fb748d..120c92e 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -7,41 +7,72 @@ # test_rdlink.sh [OPTIONS] [PATH TO COMPETE] # [OPTIONS] -# -a, --run-all : Run all tests (failed and successful) +# -a, --run-all : Run all tests (failed and successful) +# -o, --only-result : Show only result (input <> expected) # # Internal tests will be executed if no arguments are found -readonly test_dir="/opt/rdlink/test" -readonly tool_rdlink="/opt/rdlink/rdlink.sh" +readonly test_dir="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +readonly tool_rdlink="${test_dir}/../rdlink.sh" readonly tool_readlink="$(command -v readlink) -f" + +readonly config_path_width=45 + +readonly color_red='\033[1;31m' +readonly color_yellow='\033[1;33m' +readonly color_green='\033[1;32m' +readonly color_less='\033[0m' + flag_runall=0 +flag_onlyresult=0 +flag_verbose=0 toassert=() tocompete=() -. totest.sh +. "${test_dir}/totest.sh" # rl::testcmp # Compare results and print summary rl::testcmp() { - local testnum= - local expect= - local actual= - local input= - printf -- "\n%-3d --- %-30s ---" ${testnum:=${1:-0}} "${input:=${2:-"-"}}" - if [[ "${expect:=${3:-"-"}}" == "${actual:=${4:-"-"}}" ]]; then - # Test successful - printf " OK\n" - else + local testnum="${1:-0}" + local input="${2:-"-"}" + local expect="${3:-"-"}" + local actual="${4:-"-"}" + local testresult=0 #failed + local testresult_graphic="${color_red}✗${color_less}" # alt. symbol ≠ + + [[ "${expect}" == "${actual}" ]] && testresult=1 && testresult_graphic="${color_green}✔${color_less}" # alt. symbol ✓ + + # Show only reslult as quick overview + if (( flag_onlyresult )); then + # Change result graphic if test failed + (( ! testresult )) && testresult_graphic="→" # "${color_green}→${color_less}" + # Truncate input string on the left if longer than $config_path_width + (( ${#input} > ${config_path_width} )) && input="<-${input:$(( ${#input} - ${config_path_width} + 2 ))}" + # Print input and expected + printf "%${config_path_width}s %b %s\n" "${input}" "${testresult_graphic}" "${expect}" + # Print actual result if test failed + if (( ! testresult )); then + printf "%$((config_path_width))s %b %s\n" " " "${color_red}✗${color_less}" "${actual}" + return 1 + fi + return 0 + fi + + printf -- "%-3d Inp: %-${config_path_width}s ---" ${testnum} "${input}" + printf " %b\n" "${testresult_graphic}" + if (( ! testresult )); then # Test failed - printf " FAIL\n" - printf " Result:\nExp: %s\nAct: %s\n" "${expect}" "${actual}" - [ -e "${input}" ] && printf "\n Subject:\n" && ls -al "${input}" + printf " Result: %s\n Exp: %s\n" "${actual}" "${expect}" - # Print debug output of tool_a - printf "\n Debug:\n" - ( "${tool_rdlink}" -d "${input}" ) + if (( flag_verbose )); then + [ -e "${input}" ] && printf "\n Subject:\n" && ls -al "${input}" + # Print debug output of tool_a + printf "\n Debug:\n" + ( "${tool_rdlink}" -d "${input}" ) + fi return 1 fi return 0 @@ -56,12 +87,18 @@ rl::test() { local arg= for arg in "$@"; do - case "$arg" in + case "$1" in --) ## End of options shift && break ;; -a|--run-all) ## Run all even if tests fail flag_runall=1 shift ;; + -o|--only-result) + flag_onlyresult=1 + shift ;; + -v|--verbose) + flag_verbose=1 + shift ;; esac done @@ -73,13 +110,15 @@ rl::test() { fi # Compare against expected result + printf "\n# Assertion tests (\"Expected path\" == rdlink)\n" for testarray in "${toassert[@]}"; do i=0 arraywalker="$testarray"[@] for path in "${!arraywalker}"; do if (( ! i )); then # Print title in array element 0 - printf " ### %s ###\n" "$path" + printf " ### %b ###\n" "${color_yellow}${path}${color_less}" + (( flag_onlyresult )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" elif [ -z "$firstelement" ]; then # Save first element for string compare @@ -87,8 +126,8 @@ rl::test() { else # Do the compare between two following elements - if ! rl::testcmp "$((i-1))" "${path}" \ - "${firstelement}" "$("$tool_rdlink" "${path}")"; then + if ! rl::testcmp "$((i-1))" "${firstelement}" \ + "${path}" "$("$tool_rdlink" "${firstelement}")"; then # Run all tests if option -a is pressend (( ! $flag_runall )) && testend=1 && break fi @@ -101,6 +140,7 @@ rl::test() { done # Compare output of rdlink and readlink -f + printf "\n# Competition tests (readlink -f == rdlink)\n" for testarray in "${tocompete[@]}"; do i=0 arraywalker="$testarray"[@] @@ -108,7 +148,8 @@ rl::test() { if (( ! i )); then # Print title in array element 0 - printf " ### %s ###\n" "$path" + printf " ### %b ###\n" "${color_yellow}${path}${color_less}" + (( flag_onlyresult )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" else # Execute tests diff --git a/test/totest.sh b/test/totest.sh index 0bef7b3..df8f1e8 100644 --- a/test/totest.sh +++ b/test/totest.sh @@ -1,65 +1,120 @@ #!/usr/bin/env bash ########## Competition Block ########## + +# tc = test compete +_tc_tmp="${test_dir:-"/tmp"}/tmp_compete" + # Each competition link is given to both tools # and the output is compared +# +# Array format: # [0] = Title # [1] = path for competition # [2] = path for competition # ... -compete_links=( "Test - Valid links" - "/dev/stdin" - "$test_dir/tmp/a"* - "$test_dir/tmp/a 3" - "/bin/adb" -) +# Compete test suites (arrays) +compete_canonicalize=( + "Canonicalize invalid path" + "///tmp//./b" + "//tmp//./b/.." + "//tmp//./b/." +) + compete_all=( "Test - everything starting from /" /**/* ) + tocompete_init() { - # TODO initialize custom test structure + # initialize custom test structure + { + mkdir -p "${_tc_tmp}" + # compete_links + touch "${_tc_tmp}/a" + ln -s "${_tc_tmp}/a" "${_tc_tmp}/a 2" + ln -s "${_tc_tmp}/a 2" "${_tc_tmp}/a 3" + } + + # Base directory for the test + cd "${_tc_tmp}" + + # Compete test arrays with "dynamic" cases need to be inside the init function + # e.g. "$(cd ../test && pwd)/file" - base directory must be set first + # e.g. "${_tc_tmp}/"* - _tc_tmp must be populated first + compete_links=( "Test - Valid links" + "${_tc_tmp}/a 3" # slink chain a3 -> a2 -> a + "${_tc_tmp}/a"* + "/dev/stdin" + "/bin/adb" + ) # Add tests to global test array from test_rdlink tocompete+=( compete_links + compete_canonicalize + #compete_all ) } tocompete_clean() { # TODO clean custom test structure - echo "tocompete_clean" + rm -rf "${_tc_tmp}" } tocompete_init ########## Assertion Block ########## -# Assertion string compare test + +# ta = test assert +_ta_tmp="${test_dir:-"/tmp"}/tmp_assert" + +# Assertion string compare test arrays +# +# Array format # [0] = Title -# [1] expected == [2] input -# [3] expected == [4] input +# [1] input == [2] expected +# [3] input == [4] expected # ... -assert_string=( "Assert - invalid files" - "/opt/rdlink/test/a" "a" - "/opt/rdlink/test/b" "b" - "/opt/rdlink/test/c" "c" -) toassert_init() { # TODO initilaize custom test structure + { + mkdir -p "${_ta_tmp}" + } + + # Base directory for the test + cd "${_ta_tmp}" + + # Assert test arrays with "dynamic" cases need to be inside the init function + # e.g. "$(cd ../test && pwd)/file" - base directory must be set first + # e.g. "${_tc_tmp}/"* - _tc_tmp must be populated first + assert_invalid_files=( "Assert - invalid files" + "${_ta_tmp}/missing_file" "${_ta_tmp}/missing_file" + "${_ta_tmp}/missd/missf" "${_ta_tmp}/missd/missf" + "${_ta_tmp}/miss c" "${_ta_tmp}/miss c" + "rel_a" "${_ta_tmp}/rel_a" + "../rel_b" "$(cd ".." && pwd)/rel_b" + "/a/very/long/path/whthMustExceed/fourtyfive/character" "a" + ) # Add test arrays to global test array from test_rdlink toassert+=( - assert_string + assert_invalid_files ) } toassert_clean() { # TODO clean - echo "toassert_clean" + rm -rf "${_ta_tmp}" } +toassert_init + + +########## Clean custom test data ########## +# totest_cleanall() { echo tocompete_clean @@ -67,4 +122,3 @@ totest_cleanall() { } trap totest_cleanall EXIT -toassert_init -- 2.30.2 From 7f6d7007fb8f5c3ea2f5ea7c8b3f1ee1e0f01701 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Sat, 19 Mar 2022 19:57:14 +0100 Subject: [PATCH 05/18] Make brief output the default and add option -x and -xd instead of -o and -oo --- test/test_rdlink.sh | 61 ++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index 120c92e..62d3d98 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -7,8 +7,9 @@ # test_rdlink.sh [OPTIONS] [PATH TO COMPETE] # [OPTIONS] -# -a, --run-all : Run all tests (failed and successful) -# -o, --only-result : Show only result (input <> expected) +# -a, --run-all : Run all tests (failed and successful) +# -x, --extended-output : Show more information for each test +# -xd : set -x and also show rdlink debug run for failed tests # # Internal tests will be executed if no arguments are found @@ -24,7 +25,7 @@ readonly color_green='\033[1;32m' readonly color_less='\033[0m' flag_runall=0 -flag_onlyresult=0 +flag_extendedOutput=0 flag_verbose=0 toassert=() @@ -41,38 +42,39 @@ rl::testcmp() { local actual="${4:-"-"}" local testresult=0 #failed local testresult_graphic="${color_red}✗${color_less}" # alt. symbol ≠ + local truncate_graphic= [[ "${expect}" == "${actual}" ]] && testresult=1 && testresult_graphic="${color_green}✔${color_less}" # alt. symbol ✓ # Show only reslult as quick overview - if (( flag_onlyresult )); then - # Change result graphic if test failed - (( ! testresult )) && testresult_graphic="→" # "${color_green}→${color_less}" - # Truncate input string on the left if longer than $config_path_width - (( ${#input} > ${config_path_width} )) && input="<-${input:$(( ${#input} - ${config_path_width} + 2 ))}" - # Print input and expected - printf "%${config_path_width}s %b %s\n" "${input}" "${testresult_graphic}" "${expect}" - # Print actual result if test failed + if (( flag_extendedOutput )); then + printf -- "%-3d Inp: %-${config_path_width}s ---" ${testnum} "${input}" + printf " %b\n" "${testresult_graphic}" if (( ! testresult )); then - printf "%$((config_path_width))s %b %s\n" " " "${color_red}✗${color_less}" "${actual}" + # Test failed + printf " Result: %s\n Exp: %s\n" "${actual}" "${expect}" + + if (( flag_verbose )); then + [ -e "${input}" ] && printf "\n Subject:\n" && ls -al "${input}" + + # Print debug output of tool_a + printf "\n Debug:\n" + ( "${tool_rdlink}" -d "${input}" ) + fi return 1 fi return 0 fi - printf -- "%-3d Inp: %-${config_path_width}s ---" ${testnum} "${input}" - printf " %b\n" "${testresult_graphic}" + # Change result graphic if test failed + (( ! testresult )) && testresult_graphic="→" # "${color_green}→${color_less}" + # Truncate input string on the left if longer than $config_path_width + (( ${#input} > ${config_path_width} )) && input="${input:$(( ${#input} - ${config_path_width} + 1 ))}" && truncate_graphic="✀" + # Print input and expected + printf "%b%${config_path_width}s %b %s\n" "${truncate_graphic}" "${input}" "${testresult_graphic}" "${expect}" + # Print actual result if test failed if (( ! testresult )); then - # Test failed - printf " Result: %s\n Exp: %s\n" "${actual}" "${expect}" - - if (( flag_verbose )); then - [ -e "${input}" ] && printf "\n Subject:\n" && ls -al "${input}" - - # Print debug output of tool_a - printf "\n Debug:\n" - ( "${tool_rdlink}" -d "${input}" ) - fi + printf "%$((config_path_width))s %b %s\n" " " "${color_red}✗${color_less}" "${actual}" return 1 fi return 0 @@ -93,10 +95,11 @@ rl::test() { -a|--run-all) ## Run all even if tests fail flag_runall=1 shift ;; - -o|--only-result) - flag_onlyresult=1 + -x|--extended-output) + flag_extendedOutput=1 shift ;; - -v|--verbose) + -xd|--verbose) + flag_extendedOutput=1 flag_verbose=1 shift ;; esac @@ -118,7 +121,7 @@ rl::test() { if (( ! i )); then # Print title in array element 0 printf " ### %b ###\n" "${color_yellow}${path}${color_less}" - (( flag_onlyresult )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" + (( ! flag_extendedOutput )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" elif [ -z "$firstelement" ]; then # Save first element for string compare @@ -149,7 +152,7 @@ rl::test() { if (( ! i )); then # Print title in array element 0 printf " ### %b ###\n" "${color_yellow}${path}${color_less}" - (( flag_onlyresult )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" + (( ! flag_extendedOutput )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" else # Execute tests -- 2.30.2 From 08302d2680a8c6abcee95d622e6585cde3313ce1 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Sat, 19 Mar 2022 20:16:14 +0100 Subject: [PATCH 06/18] New options to only run assert or competion tests New tests for missing permissions --- test/test_rdlink.sh | 15 +++++++++++++-- test/totest.sh | 13 +++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index 62d3d98..cc4d523 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -27,6 +27,8 @@ readonly color_less='\033[0m' flag_runall=0 flag_extendedOutput=0 flag_verbose=0 +flag_onlyassert=0 +flag_onlycompete=0 toassert=() tocompete=() @@ -102,9 +104,18 @@ rl::test() { flag_extendedOutput=1 flag_verbose=1 shift ;; + -oa|--only-assert) + flag_onlyassert=1 + shift ;; + -oc|--only-compete) + flag_onlycompete=1 + shift ;; esac done + (( flag_onlyassert )) && tocompete=() + (( flag_onlycompete )) && toassert=() + # Only run `compete_args` if arguments are available if [[ "$@" ]]; then compete_args=( "Tests from command line" "$@" ) @@ -113,7 +124,7 @@ rl::test() { fi # Compare against expected result - printf "\n# Assertion tests (\"Expected path\" == rdlink)\n" + (( ${#toassert[@]} )) && printf "\n# Assertion tests (\"Expected path\" == rdlink)\n" for testarray in "${toassert[@]}"; do i=0 arraywalker="$testarray"[@] @@ -143,7 +154,7 @@ rl::test() { done # Compare output of rdlink and readlink -f - printf "\n# Competition tests (readlink -f == rdlink)\n" + (( ${#tocompete[@]} )) && printf "\n# Competition tests (readlink -f == rdlink)\n" for testarray in "${tocompete[@]}"; do i=0 arraywalker="$testarray"[@] diff --git a/test/totest.sh b/test/totest.sh index df8f1e8..391f0a5 100644 --- a/test/totest.sh +++ b/test/totest.sh @@ -35,6 +35,13 @@ tocompete_init() { touch "${_tc_tmp}/a" ln -s "${_tc_tmp}/a" "${_tc_tmp}/a 2" ln -s "${_tc_tmp}/a 2" "${_tc_tmp}/a 3" + + # compete_no_permission + mkdir -p "${_tc_tmp}/noperm" + chmod 400 "${_tc_tmp}/noperm" + ln -s "noperm" "${_tc_tmp}/lnoperm" + + #ls -l "${_tc_tmp}" } # Base directory for the test @@ -50,8 +57,14 @@ tocompete_init() { "/bin/adb" ) + compete_no_permission=( "No permission to enter directory (direct and link)" + "noperm" + "lnoperm" + ) + # Add tests to global test array from test_rdlink tocompete+=( + compete_no_permission compete_links compete_canonicalize #compete_all -- 2.30.2 From 16f5894ac53854b29074fe2bd35f17ceb692e26c Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Sun, 20 Mar 2022 03:19:36 +0100 Subject: [PATCH 07/18] Success with most tests, proceeding to more corner cases --- rdlink.sh | 63 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/rdlink.sh b/rdlink.sh index b4b6a58..29e60b7 100755 --- a/rdlink.sh +++ b/rdlink.sh @@ -17,24 +17,67 @@ set -o pipefail LOG_LEVEL="${LOG_LEVEL:-1}" # 7 = debug -> 0 = emergency rl::rdlink() { - local normalized= - info "Processing... $@" - normalized="$(rl::normalize "${1:-}")" - rl::quicklink "${normalized}" -} + local subject= -rl::canon() { - info "Canonicalize path... $@" + info "pwd: $(pwd)" + info "Processing... $@" + + subject="$(rl::normalize "${1:-}")" || true + # Follow multiple symlinks + while subject="$(rl::quicklink "${subject}")" ; do : ; done + subject="$(rl::canon "${subject}")" || true + + printf "${subject}\n" } rl::quicklink() { - info "Quick rdlink... ${1:-}" - readlink "${1:-}" || printf "${1:-}\n" + subject= + work= + + info "Quicklink... ${1:-}" + + # Check if current candidate is a symlink + if ! subject=$(readlink -- "${1:-}"); then + printf -- "${1:-}\n" + return 1 + fi + info " - symlink ${1} -> ${subject}" + + # relative target; append root + if [[ "${subject}" != "/"* ]]; then + work="$(rl::canon "$(dirname -- "${1:-}")")" + subject="${work}/${subject}" + info " - relative link resolved: ${subject}" + fi + + printf "${subject}\n" + return 0 +} + +rl::canon() { + local subject= + local work= + local bname= + + info "Canonicalize path... ${1:-}" + if work="$(cd "${1:-}" >/dev/null 2>&1 && pwd -P)" ; then + subject="${work}" + info " - directory: ${subject}" + elif work="$(cd "$(dirname -- "${1:-}")" >/dev/null 2>&1 && pwd -P)" ; then + bname="$(basename -- "${1:-}")" + subject="${work}${bname:+"/${bname}"}" + info " - parent: ${subject}" + else + info " - no hit" + subject="${1:-}" + fi + + printf "${subject}\n" } rl::normalize() { info "Normalizing... ${1:-}" - printf "${1:-}\n" + printf -- "${1:-}\n" } rl::main() { -- 2.30.2 From 5bd01ad53d85ea8a76c17a80cdc4ce009b8a3378 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Sun, 20 Mar 2022 03:22:36 +0100 Subject: [PATCH 08/18] Fix initialisation before asserts and competion New feature to exclude path with reason message (display as skipped) --- test/test_rdlink.sh | 90 +++++++++++++++++++++++++++++++++------------ test/totest.sh | 55 ++++++++++++++++++++------- 2 files changed, 107 insertions(+), 38 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index cc4d523..1c5e56a 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -10,12 +10,15 @@ # -a, --run-all : Run all tests (failed and successful) # -x, --extended-output : Show more information for each test # -xd : set -x and also show rdlink debug run for failed tests +# -oa, --only-assert : Run only assert tests +# -oc, --only-compete : Run only competion tests +# -e, --error : Print only failed tests # # Internal tests will be executed if no arguments are found readonly test_dir="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" readonly tool_rdlink="${test_dir}/../rdlink.sh" -readonly tool_readlink="$(command -v readlink) -f" +readonly tool_readlink="$(command -v readlink) -f --" readonly config_path_width=45 @@ -29,12 +32,27 @@ flag_extendedOutput=0 flag_verbose=0 flag_onlyassert=0 flag_onlycompete=0 +flag_printerror=0 toassert=() tocompete=() . "${test_dir}/totest.sh" +rl::printPath() { + local testnum="${1:-0}" + local input="${2:-"-"}" + + if (( flag_extendedOutput )); then + printf -- "%-3d Inp: %-${config_path_width}s ---" ${testnum} "${input}" + else + # Truncate input string on the left if longer than $config_path_width + (( ${#input} > ${config_path_width} )) && input="${input:$(( ${#input} - ${config_path_width} + 1 ))}" && truncate_graphic="✀" + # Print input and expected + printf "%b%${config_path_width}s" "${truncate_graphic}" "${input}" + fi +} + # rl::testcmp # Compare results and print summary rl::testcmp() { @@ -46,11 +64,16 @@ rl::testcmp() { local testresult_graphic="${color_red}✗${color_less}" # alt. symbol ≠ local truncate_graphic= - [[ "${expect}" == "${actual}" ]] && testresult=1 && testresult_graphic="${color_green}✔${color_less}" # alt. symbol ✓ + if [[ "${expect}" == "${actual}" ]] ; then + # Don't print success for this flag + (( flag_printerror )) && return 0 + testresult=1 + testresult_graphic="${color_green}✔${color_less}" # alt. symbol ✓ + fi # Show only reslult as quick overview if (( flag_extendedOutput )); then - printf -- "%-3d Inp: %-${config_path_width}s ---" ${testnum} "${input}" + rl::printPath ${testnum} "${input}" printf " %b\n" "${testresult_graphic}" if (( ! testresult )); then # Test failed @@ -61,7 +84,7 @@ rl::testcmp() { # Print debug output of tool_a printf "\n Debug:\n" - ( "${tool_rdlink}" -d "${input}" ) + ( ${tool_rdlink} -d -- "${input}" ) fi return 1 fi @@ -70,10 +93,8 @@ rl::testcmp() { # Change result graphic if test failed (( ! testresult )) && testresult_graphic="→" # "${color_green}→${color_less}" - # Truncate input string on the left if longer than $config_path_width - (( ${#input} > ${config_path_width} )) && input="${input:$(( ${#input} - ${config_path_width} + 1 ))}" && truncate_graphic="✀" - # Print input and expected - printf "%b%${config_path_width}s %b %s\n" "${truncate_graphic}" "${input}" "${testresult_graphic}" "${expect}" + rl::printPath "" "${input}" + printf " %b %s\n" "${testresult_graphic}" "${expect}" # Print actual result if test failed if (( ! testresult )); then printf "%$((config_path_width))s %b %s\n" " " "${color_red}✗${color_less}" "${actual}" @@ -89,6 +110,7 @@ rl::test() { local firstelement= local testend=0 local arg= + local excludemsg= for arg in "$@"; do case "$1" in @@ -110,21 +132,22 @@ rl::test() { -oc|--only-compete) flag_onlycompete=1 shift ;; + -e|--error) + flag_printerror=1 + shift ;; + -*|--*) + printf "Invalid argument\n" + exit 1 ;; esac done - (( flag_onlyassert )) && tocompete=() - (( flag_onlycompete )) && toassert=() - - # Only run `compete_args` if arguments are available - if [[ "$@" ]]; then - compete_args=( "Tests from command line" "$@" ) - (( flag_runall )) && tocompete+=(compete_args) || tocompete=(compete_args) - toassert=() - fi + # Cmd line arguments + (( ! flag_onlycompete )) && [[ ! "$@" ]] && toassert_init # Compare against expected result - (( ${#toassert[@]} )) && printf "\n# Assertion tests (\"Expected path\" == rdlink)\n" + if (( ${#toassert[@]} )) ; then + printf "\n# Assertion tests (\"Expected path\" == rdlink)\n" + fi for testarray in "${toassert[@]}"; do i=0 arraywalker="$testarray"[@] @@ -138,10 +161,14 @@ rl::test() { # Save first element for string compare firstelement="${path}" + # Check for excludes + elif excludemsg="$(toexclude "${firstelement}")"; then + rl::printPath "$((i-1))" "${firstelement}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" + # Execute tests else # Do the compare between two following elements if ! rl::testcmp "$((i-1))" "${firstelement}" \ - "${path}" "$("$tool_rdlink" "${firstelement}")"; then + "${path}" "$($tool_rdlink -- "${firstelement}")"; then # Run all tests if option -a is pressend (( ! $flag_runall )) && testend=1 && break fi @@ -152,9 +179,20 @@ rl::test() { done (( testend )) && return 1 done - + + # Initialize competition tests + (( ! flag_onlyassert )) && tocompete_init + + # Only run `compete_args` if arguments are available + if [[ "$@" ]]; then + compete_args=( "Tests from command line" "$@" ) + tocompete=(compete_args) + fi + # Compare output of rdlink and readlink -f - (( ${#tocompete[@]} )) && printf "\n# Competition tests (readlink -f == rdlink)\n" + if (( ${#tocompete[@]} )) ; then + printf "\n# Competition tests (readlink -f == rdlink)\n" + fi for testarray in "${tocompete[@]}"; do i=0 arraywalker="$testarray"[@] @@ -166,9 +204,12 @@ rl::test() { (( ! flag_extendedOutput )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" else + # Check for excludes + if excludemsg="$(toexclude "${path}")" ; then + rl::printPath "" "${path}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" # Execute tests - if ! rl::testcmp "${i}" "${path}" \ - "$(${tool_readlink} "$path")" "$(${tool_rdlink} "$path")"; then + elif ! rl::testcmp "${i}" "${path}" \ + "$(${tool_readlink} "$path")" "$(${tool_rdlink} -- "$path")"; then # Run all tests if option -a is pressend (( ! $flag_runall )) && testend=1 && break fi @@ -180,4 +221,5 @@ rl::test() { done } -time rl::test "$@" +#time rl::test "$@" +rl::test "$@" diff --git a/test/totest.sh b/test/totest.sh index 391f0a5..ca7fa74 100644 --- a/test/totest.sh +++ b/test/totest.sh @@ -15,11 +15,11 @@ _tc_tmp="${test_dir:-"/tmp"}/tmp_compete" # ... # Compete test suites (arrays) -compete_canonicalize=( - "Canonicalize invalid path" +compete_canonicalize=( "Canonicalize invalid path" + "-v" # Not recommended file naming "///tmp//./b" - "//tmp//./b/.." - "//tmp//./b/." + #"//tmp//./b/.." # TODO return empty + #"//tmp//./b/." # TODO return empty ) compete_all=( "Test - everything starting from /" @@ -30,6 +30,7 @@ compete_all=( "Test - everything starting from /" tocompete_init() { # initialize custom test structure { + tocompete_clean mkdir -p "${_tc_tmp}" # compete_links touch "${_tc_tmp}/a" @@ -40,7 +41,9 @@ tocompete_init() { mkdir -p "${_tc_tmp}/noperm" chmod 400 "${_tc_tmp}/noperm" ln -s "noperm" "${_tc_tmp}/lnoperm" + ln -s "/" "${_tc_tmp}/lroot" + #echo "rl: " && readlink "${_tc_tmp}/lnoperm" #ls -l "${_tc_tmp}" } @@ -55,11 +58,16 @@ tocompete_init() { "${_tc_tmp}/a"* "/dev/stdin" "/bin/adb" + "/dev/fd" # Test skip - /dev/fd is different on every call + "/dev/stdout" # skip - different output + "/etc/mtab" # skip - Always different + "/proc/mounts" # skip - Always different ) compete_no_permission=( "No permission to enter directory (direct and link)" - "noperm" "lnoperm" + "noperm" + "lroot" ) # Add tests to global test array from test_rdlink @@ -67,17 +75,14 @@ tocompete_init() { compete_no_permission compete_links compete_canonicalize - #compete_all + compete_all ) } tocompete_clean() { - # TODO clean custom test structure rm -rf "${_tc_tmp}" } -tocompete_init - ########## Assertion Block ########## # ta = test assert @@ -92,7 +97,6 @@ _ta_tmp="${test_dir:-"/tmp"}/tmp_assert" # ... toassert_init() { - # TODO initilaize custom test structure { mkdir -p "${_ta_tmp}" } @@ -109,7 +113,6 @@ toassert_init() { "${_ta_tmp}/miss c" "${_ta_tmp}/miss c" "rel_a" "${_ta_tmp}/rel_a" "../rel_b" "$(cd ".." && pwd)/rel_b" - "/a/very/long/path/whthMustExceed/fourtyfive/character" "a" ) # Add test arrays to global test array from test_rdlink @@ -119,17 +122,41 @@ toassert_init() { } toassert_clean() { - # TODO clean rm -rf "${_ta_tmp}" } -toassert_init +########## Common data and functions ########## +# +# [0] path to exclude [1] Reason for exclution +# [2] path to exclude [3] Reason for exclution +exclude_path=( + "/dev/fd" "Always different" + "/dev/stdout" "Always different in pipes" + "/etc/mtab" "Always different" + "/proc/mounts" "Always different" +) +toexclude() { + local path= + local exclude= + for path in "${exclude_path[@]}" ; do + if [[ ! "${exclude}" ]] ; then + exclude="${path}" + else + # return reason for exclution + if [[ "${exclude}" == "${1:-}" ]] ; then + printf "${path}" + return 0 + fi + exclude= + fi + done + return 1 +} ########## Clean custom test data ########## # totest_cleanall() { - echo tocompete_clean toassert_clean } -- 2.30.2 From dcabe75887d0560e82d22555cb1ef6c320f0c367 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Sun, 20 Mar 2022 14:33:39 +0100 Subject: [PATCH 09/18] Beta stage: traverses through /**/* without error (some files excluded; see test/totest.sh) --- rdlink.sh | 84 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/rdlink.sh b/rdlink.sh index 29e60b7..f951514 100755 --- a/rdlink.sh +++ b/rdlink.sh @@ -18,14 +18,45 @@ LOG_LEVEL="${LOG_LEVEL:-1}" # 7 = debug -> 0 = emergency rl::rdlink() { local subject= + local work= + local resolved=0 info "pwd: $(pwd)" info "Processing... $@" subject="$(rl::normalize "${1:-}")" || true # Follow multiple symlinks - while subject="$(rl::quicklink "${subject}")" ; do : ; done - subject="$(rl::canon "${subject}")" || true + while subject="$(rl::quicklink "${subject}")" ; do + # A link was resolved at least once + info "rl::rdlink - Link found: $subject" + resolved=1 + done + + # Special cases handling + { + # If subject is still a link, after rl::quicklink call(s) + # current user has no permission to access the link itself + # (e.g. /proc/**/cwd) + if [ -L "${subject}" ] ; then + info "rl::rdlink exit - can't access link ${subject}" + printf "\n" + return 1 + fi + } + + # Empty output if (dirname $subject) is not a valid path + if ! work="$(rl::canon "${subject}")" ; then + # Special: Links resolved to something like: /proc/2267178/fd/pipe:[22306727] + # are not existing files but `readlink -f` prints result anyway + # Printing result if a link was resolved at least once + if (( ! resolved )); then + info "rl::rdlink exit - invalid path ${subject}" + printf "\n" + return 1 + fi + else + subject="${work}" + fi printf "${subject}\n" } @@ -43,13 +74,13 @@ rl::quicklink() { fi info " - symlink ${1} -> ${subject}" - # relative target; append root + # relative symlink target; prepend its parent direcotry if [[ "${subject}" != "/"* ]]; then work="$(rl::canon "$(dirname -- "${1:-}")")" subject="${work}/${subject}" info " - relative link resolved: ${subject}" fi - + printf "${subject}\n" return 0 } @@ -61,23 +92,40 @@ rl::canon() { info "Canonicalize path... ${1:-}" if work="$(cd "${1:-}" >/dev/null 2>&1 && pwd -P)" ; then - subject="${work}" + # Special: `pwd -P` returns with // as root for links starting at / + # e.g. $(readlink /proc/self/root) == "/" + # $(cd /proc/self/root/mnt && pwd -P) == "//mnt" + subject="$(rl::normalize "${work}")" info " - directory: ${subject}" elif work="$(cd "$(dirname -- "${1:-}")" >/dev/null 2>&1 && pwd -P)" ; then bname="$(basename -- "${1:-}")" + + # Special: / produces // + [[ "${work}" == "/" ]] && work= + subject="${work}${bname:+"/${bname}"}" info " - parent: ${subject}" else - info " - no hit" + info " - no hit for: ${1:-}" subject="${1:-}" + return 1 fi - printf "${subject}\n" + printf -- "${subject}\n" + return 0 } rl::normalize() { + local work= info "Normalizing... ${1:-}" - printf -- "${1:-}\n" + + work="${1:-}" + # Remove multiple / + while [[ "${work:-}" = *"//"* ]]; do + work="${work//'//'/'/'}" + done + + printf -- "${work}\n" } rl::main() { @@ -118,20 +166,20 @@ function rl::log () { printf "%s [%9s] %s\n" "$(date -u +"%Y-%m-%d %H:%M:%S UTC")" "${log_level}" "${log_line}" 1>&2 done <<< "${@:-}" } -emergency () { rl::log emergency "${@}"; exit 1; } -alarm() { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && rl::log alert "${@}"; true; } -critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && rl::log critical "${@}"; true; } -error () { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && rl::log error "${@}"; true; } -warning () { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && rl::log warning "${@}"; true; } -notice () { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && rl::log notice "${@}"; true; } -info () { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && rl::log info "${@}"; true; } -debug () { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && rl::log debug "${@}"; true; } -msg () { echo "$@"; true; } +emergency() { rl::log emergency "${@}"; exit 1; } +alarm() { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && rl::log alert "${@}"; true; } +critical() { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && rl::log critical "${@}"; true; } +error() { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && rl::log error "${@}"; true; } +warning() { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && rl::log warning "${@}"; true; } +notice() { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && rl::log notice "${@}"; true; } +info() { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && rl::log info "${@}"; true; } +debug() { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && rl::log debug "${@}"; true; } +msg() { echo "$@"; true; } # Provide as function to be called when sourced rdlink() { - rl::main "$@" + rl::rdlink "$@" } ### Check if script is _sourced -- 2.30.2 From 3d9113377c483a1e6fc893bee94b68273478f7b0 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Sun, 20 Mar 2022 14:37:18 +0100 Subject: [PATCH 10/18] Adding more tests and excludes to be able to traverse through /**/* wihtout error Added test summary to stdout --- test/test_rdlink.sh | 44 +++++++++++++++++++++++++++++++++++++++----- test/totest.sh | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index 1c5e56a..375d3a3 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -53,6 +53,25 @@ rl::printPath() { fi } +rl::printTestSummary() { + local success=${1:-0} + local failed=${2:-0} + local skipped=${3:-0} + local total=$(( success + failed + skipped )) + local separator="--------------------" + + cat < # Compare results and print summary rl::testcmp() { @@ -111,6 +130,9 @@ rl::test() { local testend=0 local arg= local excludemsg= + local tests_success=0 + local tests_failed=0 + local tests_skipped=0 for arg in "$@"; do case "$1" in @@ -163,22 +185,27 @@ rl::test() { # Check for excludes elif excludemsg="$(toexclude "${firstelement}")"; then - rl::printPath "$((i-1))" "${firstelement}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" + (( tests_skipped++ )) + rl::printPath "$(( i/2 ))" "${firstelement}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" # Execute tests else # Do the compare between two following elements - if ! rl::testcmp "$((i-1))" "${firstelement}" \ + if ! rl::testcmp "$(( i/2 ))" "${firstelement}" \ "${path}" "$($tool_rdlink -- "${firstelement}")"; then + (( tests_failed++ )) # Run all tests if option -a is pressend (( ! $flag_runall )) && testend=1 && break + else + (( tests_success++ )) fi firstelement= fi ((i++)) done - (( testend )) && return 1 + (( testend )) && break done + (( testend )) && rl::printTestSummary $tests_success $tests_failed $tests_skipped && return 1 # Initialize competition tests (( ! flag_onlyassert )) && tocompete_init @@ -206,19 +233,26 @@ rl::test() { else # Check for excludes if excludemsg="$(toexclude "${path}")" ; then - rl::printPath "" "${path}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" + (( tests_skipped++ )) + rl::printPath "${i}" "${path}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" # Execute tests elif ! rl::testcmp "${i}" "${path}" \ "$(${tool_readlink} "$path")" "$(${tool_rdlink} -- "$path")"; then + (( tests_failed++ )) # Run all tests if option -a is pressend (( ! $flag_runall )) && testend=1 && break + else + (( tests_success++ )) fi fi ((i++)) done - (( testend )) && return 1 + (( testend )) && break done + + rl::printTestSummary $tests_success $tests_failed $tests_skipped + (( testend )) && return 1 || return 0 } #time rl::test "$@" diff --git a/test/totest.sh b/test/totest.sh index ca7fa74..8db7848 100644 --- a/test/totest.sh +++ b/test/totest.sh @@ -40,8 +40,9 @@ tocompete_init() { # compete_no_permission mkdir -p "${_tc_tmp}/noperm" chmod 400 "${_tc_tmp}/noperm" - ln -s "noperm" "${_tc_tmp}/lnoperm" + ln -s "noperm" "${_tc_tmp}/noperml" ln -s "/" "${_tc_tmp}/lroot" + ln -s "/root" "${_tc_tmp}/lroothome" #echo "rl: " && readlink "${_tc_tmp}/lnoperm" #ls -l "${_tc_tmp}" @@ -53,6 +54,12 @@ tocompete_init() { # Compete test arrays with "dynamic" cases need to be inside the init function # e.g. "$(cd ../test && pwd)/file" - base directory must be set first # e.g. "${_tc_tmp}/"* - _tc_tmp must be populated first + compete_invalid=( "Invalid files with and without valid path" + "${_tc_tmp}/invalid_file" + "/invalid_file" + "/invalid_direcotry/invalidfile" + ) + compete_links=( "Test - Valid links" "${_tc_tmp}/a 3" # slink chain a3 -> a2 -> a "${_tc_tmp}/a"* @@ -62,18 +69,21 @@ tocompete_init() { "/dev/stdout" # skip - different output "/etc/mtab" # skip - Always different "/proc/mounts" # skip - Always different + "/proc/net/"* # skip - Always different ) compete_no_permission=( "No permission to enter directory (direct and link)" - "lnoperm" - "noperm" - "lroot" + "/proc/"**/cwd # special - no permission for links + "noperm"* + "lroot"* + "/" ) # Add tests to global test array from test_rdlink tocompete+=( - compete_no_permission + compete_invalid compete_links + compete_no_permission compete_canonicalize compete_all ) @@ -109,7 +119,7 @@ toassert_init() { # e.g. "${_tc_tmp}/"* - _tc_tmp must be populated first assert_invalid_files=( "Assert - invalid files" "${_ta_tmp}/missing_file" "${_ta_tmp}/missing_file" - "${_ta_tmp}/missd/missf" "${_ta_tmp}/missd/missf" + "${_ta_tmp}/missd/missf" "" "${_ta_tmp}/miss c" "${_ta_tmp}/miss c" "rel_a" "${_ta_tmp}/rel_a" "../rel_b" "$(cd ".." && pwd)/rel_b" @@ -127,13 +137,19 @@ toassert_clean() { ########## Common data and functions ########## # -# [0] path to exclude [1] Reason for exclution -# [2] path to exclude [3] Reason for exclution +# [0] exclude regex [1] Reason for exclution +# [2] exclude regex [3] Reason for exclution exclude_path=( - "/dev/fd" "Always different" - "/dev/stdout" "Always different in pipes" - "/etc/mtab" "Always different" - "/proc/mounts" "Always different" + "/dev/fd$" "Different on every call" + "/dev/stdout$" "Always different in pipes" + "/etc/mtab$" "Different on every call" + "/proc/mounts$" "Different on every call" + "/proc/net$" "Different on every call" + "/proc/net/.*" "Different on every call" + "/proc/self/fd/(1|3|255)" "Different on every call" + "/proc/self$" "Different on every call" + "/proc/self/(attr|fdinfo|map_files|net|ns|task)/.*" "Different on every call" + "/proc/thread-self$" "Different on every call" ) toexclude() { @@ -144,7 +160,7 @@ toexclude() { exclude="${path}" else # return reason for exclution - if [[ "${exclude}" == "${1:-}" ]] ; then + if [[ "${1:-}" =~ ${exclude} ]] ; then printf "${path}" return 0 fi -- 2.30.2 From 621da5e4c81ceaefe43dbad68b6065802a313b11 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Mon, 21 Mar 2022 14:56:44 +0100 Subject: [PATCH 11/18] Fix processingi of irregular pathes liks ///tmp//./b/. --- rdlink.sh | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/rdlink.sh b/rdlink.sh index f951514..e7a147f 100755 --- a/rdlink.sh +++ b/rdlink.sh @@ -21,10 +21,11 @@ rl::rdlink() { local work= local resolved=0 - info "pwd: $(pwd)" - info "Processing... $@" + info "rd::link Processing... $@" + info " pwd: $(pwd)" + + subject="$(rl::cleanpath "${1:-}")" || true - subject="$(rl::normalize "${1:-}")" || true # Follow multiple symlinks while subject="$(rl::quicklink "${subject}")" ; do # A link was resolved at least once @@ -49,11 +50,11 @@ rl::rdlink() { # Special: Links resolved to something like: /proc/2267178/fd/pipe:[22306727] # are not existing files but `readlink -f` prints result anyway # Printing result if a link was resolved at least once - if (( ! resolved )); then + #if (( ! resolved )); then info "rl::rdlink exit - invalid path ${subject}" printf "\n" return 1 - fi + #fi else subject="${work}" fi @@ -115,16 +116,50 @@ rl::canon() { return 0 } -rl::normalize() { +rl::cleanpath() { local work= - info "Normalizing... ${1:-}" + local rex_tmp= + info "rl::cleanpath...${1:-}" + work="${1:-}" # Remove multiple / while [[ "${work:-}" = *"//"* ]]; do work="${work//'//'/'/'}" done + + info "rl::cleanpath result: ${work}" + printf -- "${work}\n" +} +rl::normalize() { + local work= + + info "rl::normalize...${1:-}" + + work="${1:-}" + # Remove dir/.. sequences. + local rex_tmp='[^/][^/]*/\.\./*' + while [[ "${work}" =~ $rex_tmp ]] ; do + work="${work/"${BASH_REMATCH[0]}"/}" + done + + # Remove /./ or /.$ sequences. + rex_tmp='/\.(/|$)' + while [[ "$work" =~ $rex_tmp ]]; do + work="${work/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]/$//}"}" + done + + # Remove leading ./ + work="${work#./*}" + + # Remove trailing / + rex_tmp='(.*[^/])/$' + if [[ "${work}" =~ $rex_tmp ]] ; then + work="${BASH_REMATCH[1]}" + fi + + info "rl::normalize result: ${work}" printf -- "${work}\n" } -- 2.30.2 From 333e2268712e548ccd9d1d2604f95d24dc163473 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Mon, 21 Mar 2022 22:41:54 +0100 Subject: [PATCH 12/18] Add some more special handlings Smaller updates of debug output --- rdlink.sh | 80 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/rdlink.sh b/rdlink.sh index e7a147f..03a5a11 100755 --- a/rdlink.sh +++ b/rdlink.sh @@ -21,8 +21,8 @@ rl::rdlink() { local work= local resolved=0 - info "rd::link Processing... $@" - info " pwd: $(pwd)" + info "Processing: $@" + info " with pwd: $(pwd)" subject="$(rl::cleanpath "${1:-}")" || true @@ -51,7 +51,7 @@ rl::rdlink() { # are not existing files but `readlink -f` prints result anyway # Printing result if a link was resolved at least once #if (( ! resolved )); then - info "rl::rdlink exit - invalid path ${subject}" + info "rl::rdlink exit - invalid path ${work}" printf "\n" return 1 #fi @@ -90,37 +90,65 @@ rl::canon() { local subject= local work= local bname= + local run=1 + local start= + local retval=0 - info "Canonicalize path... ${1:-}" - if work="$(cd "${1:-}" >/dev/null 2>&1 && pwd -P)" ; then - # Special: `pwd -P` returns with // as root for links starting at / - # e.g. $(readlink /proc/self/root) == "/" - # $(cd /proc/self/root/mnt && pwd -P) == "//mnt" - subject="$(rl::normalize "${work}")" - info " - directory: ${subject}" - elif work="$(cd "$(dirname -- "${1:-}")" >/dev/null 2>&1 && pwd -P)" ; then - bname="$(basename -- "${1:-}")" + start="${1:-}" + info "Canonicalize path... ${start}" - # Special: / produces // - [[ "${work}" == "/" ]] && work= + while (( run )) ; do + if work="$(cd "${start}" >/dev/null 2>&1 && pwd -P)" ; then + # Special: `pwd -P` returns with // as root for links starting at / + # e.g. $(readlink /proc/self/root) == "/" + # $(cd /proc/self/root/mnt && pwd -P) == "//mnt" + subject="$(rl::normalize "${work}")" + info "rl::canon valid directory: ${subject}" + run=0 + elif work="$(cd "$(dirname -- "${start}")" >/dev/null 2>&1 && pwd -P)" ; then + bname="$(basename -- "${start}")" - subject="${work}${bname:+"/${bname}"}" - info " - parent: ${subject}" - else - info " - no hit for: ${1:-}" - subject="${1:-}" - return 1 - fi + # Special: / produces // + [[ "${work}" == "/" ]] && work= + + subject="${work}${bname:+"/${bname}"}" + info "rl::canon valid parent: ${subject}" + # Special: Succeed with valid element after second run; see special below + # e.g. /root/. + # * /root is valid but not accessible + if (( retval )) && [ -e "${subject}" ] ;then + info "rl::canon valid element" + retval=0 + fi + run=0 + else + # Special: Some paths may be resolvable after normalization + # e.g. somedir/.. + # * base "somedir" does not exist + # * but irrelevant because of /.. + # * resolves to pwd but fails by readlink -f + work="$(rl::normalize "${start}")" + if [[ "${work}" != "${start}" ]] ; then + info "rl::canon retry with: ${work}" + start="${work}" + retval=1 + continue + fi + info "rl::canon invalid path: ${work}" + subject="${work}" + run=0 && retval=1 + fi + done printf -- "${subject}\n" - return 0 + return ${retval} } rl::cleanpath() { local work= local rex_tmp= - info "rl::cleanpath...${1:-}" + info "Cleaning path... ${1:-}" work="${1:-}" # Remove multiple / @@ -135,16 +163,16 @@ rl::cleanpath() { rl::normalize() { local work= - info "rl::normalize...${1:-}" + info "Normalizing path... ${1:-}" - work="${1:-}" + work="${1:-}" # Remove dir/.. sequences. local rex_tmp='[^/][^/]*/\.\./*' while [[ "${work}" =~ $rex_tmp ]] ; do work="${work/"${BASH_REMATCH[0]}"/}" done - # Remove /./ or /.$ sequences. + # Remove /./ and /.$ sequences. rex_tmp='/\.(/|$)' while [[ "$work" =~ $rex_tmp ]]; do work="${work/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]/$//}"}" -- 2.30.2 From cac8014f151d30dc29fe99acbcbb746493d477b0 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Mon, 21 Mar 2022 22:43:55 +0100 Subject: [PATCH 13/18] Fix truncate grahic placing and reduce summary to two lines --- test/test_rdlink.sh | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index 375d3a3..baab27b 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -42,14 +42,22 @@ tocompete=() rl::printPath() { local testnum="${1:-0}" local input="${2:-"-"}" + local truncate_graphic= + local inputwidth=${config_path_width} if (( flag_extendedOutput )); then printf -- "%-3d Inp: %-${config_path_width}s ---" ${testnum} "${input}" else # Truncate input string on the left if longer than $config_path_width - (( ${#input} > ${config_path_width} )) && input="${input:$(( ${#input} - ${config_path_width} + 1 ))}" && truncate_graphic="✀" + if (( ${#input} > ${config_path_width} )) ; then + # +1 : prepending truncate_graphic + input="${input:$(( ${#input} - ${config_path_width} + 1 ))}" + # -1 : prepending truncate_graphic + inputwidth=$((config_path_width - 1)) + truncate_graphic="✀" + fi # Print input and expected - printf "%b%${config_path_width}s" "${truncate_graphic}" "${input}" + printf "%b%${inputwidth}s" "${truncate_graphic}" "${input}" fi } @@ -58,18 +66,11 @@ rl::printTestSummary() { local failed=${2:-0} local skipped=${3:-0} local total=$(( success + failed + skipped )) - local separator="--------------------" + readonly columnwidth=7 + readonly tableformat="%${columnwidth}s | %${columnwidth}s | %${columnwidth}s | %${columnwidth}s\n" - cat < @@ -81,7 +82,6 @@ rl::testcmp() { local actual="${4:-"-"}" local testresult=0 #failed local testresult_graphic="${color_red}✗${color_less}" # alt. symbol ≠ - local truncate_graphic= if [[ "${expect}" == "${actual}" ]] ; then # Don't print success for this flag -- 2.30.2 From 5cea7b41983a3b5d510f54488588f6d205cb96b6 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Mon, 21 Mar 2022 22:45:28 +0100 Subject: [PATCH 14/18] More test cases --- test/totest.sh | 57 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/test/totest.sh b/test/totest.sh index 8db7848..d6fe2b8 100644 --- a/test/totest.sh +++ b/test/totest.sh @@ -16,10 +16,19 @@ _tc_tmp="${test_dir:-"/tmp"}/tmp_compete" # Compete test suites (arrays) compete_canonicalize=( "Canonicalize invalid path" - "-v" # Not recommended file naming "///tmp//./b" - #"//tmp//./b/.." # TODO return empty - #"//tmp//./b/." # TODO return empty + "//tmp//./b/.." + "//tmp//./b/." + "//tmp//./b/" + "///notthere//...../help/." +) + +compete_relative=( "Resolving of relative links" + # Invalid + "nofile_l" + "nopath_l" + # Valid + "dir_1/file" # Valid relative link to path with relative element ) compete_all=( "Test - everything starting from /" @@ -30,12 +39,18 @@ compete_all=( "Test - everything starting from /" tocompete_init() { # initialize custom test structure { - tocompete_clean mkdir -p "${_tc_tmp}" + cd "${_tc_tmp}" + # compete_links touch "${_tc_tmp}/a" ln -s "${_tc_tmp}/a" "${_tc_tmp}/a 2" ln -s "${_tc_tmp}/a 2" "${_tc_tmp}/a 3" + mkdir -p "dir_1" + mkdir -p "dir_2/dir_22" + touch "dir_2/dir_22/file" + ln -s "dir_2/dir_22" "dir_3_l" + ln -s "../dir_3_l/file" "dir_1/file" # compete_no_permission mkdir -p "${_tc_tmp}/noperm" @@ -43,14 +58,19 @@ tocompete_init() { ln -s "noperm" "${_tc_tmp}/noperml" ln -s "/" "${_tc_tmp}/lroot" ln -s "/root" "${_tc_tmp}/lroothome" + + # compete_relative + ln -s "../nofile" "${_tc_tmp}/nofile_l" + ln -s "../nodir/nofile" "${_tc_tmp}/nopath_l" #echo "rl: " && readlink "${_tc_tmp}/lnoperm" #ls -l "${_tc_tmp}" } - + # Base directory for the test cd "${_tc_tmp}" + # Compete test arrays with "dynamic" cases need to be inside the init function # e.g. "$(cd ../test && pwd)/file" - base directory must be set first # e.g. "${_tc_tmp}/"* - _tc_tmp must be populated first @@ -58,33 +78,40 @@ tocompete_init() { "${_tc_tmp}/invalid_file" "/invalid_file" "/invalid_direcotry/invalidfile" + "-v" # Not recommended file naming + "../-v" # Not recommended file naming + "-v/.." # Not recommended file naming ) compete_links=( "Test - Valid links" "${_tc_tmp}/a 3" # slink chain a3 -> a2 -> a "${_tc_tmp}/a"* "/dev/stdin" - "/bin/adb" - "/dev/fd" # Test skip - /dev/fd is different on every call - "/dev/stdout" # skip - different output - "/etc/mtab" # skip - Always different - "/proc/mounts" # skip - Always different - "/proc/net/"* # skip - Always different + #"/dev/stdout" skip - Always different + #"/dev/fd" # Test skip - /dev/fd is different on every call + #"/etc/mtab" # skip - Always different + #"/proc/mounts" # skip - Always different + #"/proc/net/"* # skip - Always different ) compete_no_permission=( "No permission to enter directory (direct and link)" - "/proc/"**/cwd # special - no permission for links "noperm"* "lroot"* "/" + "/root" + "/root/" + "/root/." + "/root/.." + "/proc/"**/cwd # special - no permission for links ) # Add tests to global test array from test_rdlink tocompete+=( - compete_invalid - compete_links - compete_no_permission compete_canonicalize + compete_invalid + compete_relative + compete_no_permission + compete_links compete_all ) } -- 2.30.2 From 571791fd5cadd1171e4868f85159113a3a25406e Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Tue, 22 Mar 2022 10:20:42 +0100 Subject: [PATCH 15/18] Fixing shellcheck errors and prepare for build.sh --- rdlink.sh | 105 ++++++++++++++++++++++++++---------------------------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/rdlink.sh b/rdlink.sh index 03a5a11..4b8eb37 100755 --- a/rdlink.sh +++ b/rdlink.sh @@ -16,12 +16,33 @@ set -o pipefail LOG_LEVEL="${LOG_LEVEL:-1}" # 7 = debug -> 0 = emergency +function rl::log () { + local log_level="${1:-${LOG_LEVEL}}" + shift + + # all remaining arguments are to be printed + local log_line="" + + while IFS=$'\n' read -r log_line; do + printf "%s [%9s] %s\n" "$(date -u +"%Y-%m-%d %H:%M:%S UTC")" "${log_level}" "${log_line}" 1>&2 + done <<< "${@:-}" +} +emergency() { rl::log emergency "${@}"; exit 1; } +alarm() { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && rl::log alert "${@}"; true; } +critical() { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && rl::log critical "${@}"; true; } +error() { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && rl::log error "${@}"; true; } +warning() { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && rl::log warning "${@}"; true; } +notice() { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && rl::log notice "${@}"; true; } +info() { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && rl::log info "${@}"; true; } +debug() { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && rl::log debug "${@}"; true; } +msg() { echo "$@"; true; } + +### Script rl::rdlink() { local subject= local work= - local resolved=0 - info "Processing: $@" + info "Processing: $*" info " with pwd: $(pwd)" subject="$(rl::cleanpath "${1:-}")" || true @@ -29,8 +50,8 @@ rl::rdlink() { # Follow multiple symlinks while subject="$(rl::quicklink "${subject}")" ; do # A link was resolved at least once - info "rl::rdlink - Link found: $subject" - resolved=1 + : + info " rl::rdlink - Link found: $subject" done # Special cases handling @@ -39,7 +60,7 @@ rl::rdlink() { # current user has no permission to access the link itself # (e.g. /proc/**/cwd) if [ -L "${subject}" ] ; then - info "rl::rdlink exit - can't access link ${subject}" + info " rl::rdlink exit - can't access link ${subject}" printf "\n" return 1 fi @@ -47,19 +68,14 @@ rl::rdlink() { # Empty output if (dirname $subject) is not a valid path if ! work="$(rl::canon "${subject}")" ; then - # Special: Links resolved to something like: /proc/2267178/fd/pipe:[22306727] - # are not existing files but `readlink -f` prints result anyway - # Printing result if a link was resolved at least once - #if (( ! resolved )); then - info "rl::rdlink exit - invalid path ${work}" - printf "\n" - return 1 - #fi + info " rl::rdlink exit - invalid path ${work}" + printf "\n" + return 1 else subject="${work}" fi - printf "${subject}\n" + printf "%s\n" "${subject}" } rl::quicklink() { @@ -70,19 +86,19 @@ rl::quicklink() { # Check if current candidate is a symlink if ! subject=$(readlink -- "${1:-}"); then - printf -- "${1:-}\n" + printf -- "%s\n" "${1:-}" return 1 fi - info " - symlink ${1} -> ${subject}" + info " rl::quicklink symlink ${1} -> ${subject}" # relative symlink target; prepend its parent direcotry if [[ "${subject}" != "/"* ]]; then work="$(rl::canon "$(dirname -- "${1:-}")")" subject="${work}/${subject}" - info " - relative link resolved: ${subject}" + info " rl::quicklink relative link resolved: ${subject}" fi - printf "${subject}\n" + printf "%s\n" "${subject}" return 0 } @@ -103,7 +119,7 @@ rl::canon() { # e.g. $(readlink /proc/self/root) == "/" # $(cd /proc/self/root/mnt && pwd -P) == "//mnt" subject="$(rl::normalize "${work}")" - info "rl::canon valid directory: ${subject}" + info " rl::canon valid directory: ${subject}" run=0 elif work="$(cd "$(dirname -- "${start}")" >/dev/null 2>&1 && pwd -P)" ; then bname="$(basename -- "${start}")" @@ -112,12 +128,12 @@ rl::canon() { [[ "${work}" == "/" ]] && work= subject="${work}${bname:+"/${bname}"}" - info "rl::canon valid parent: ${subject}" + info " rl::canon valid parent: ${subject}" # Special: Succeed with valid element after second run; see special below # e.g. /root/. # * /root is valid but not accessible if (( retval )) && [ -e "${subject}" ] ;then - info "rl::canon valid element" + info " rl::canon valid element" retval=0 fi run=0 @@ -129,18 +145,18 @@ rl::canon() { # * resolves to pwd but fails by readlink -f work="$(rl::normalize "${start}")" if [[ "${work}" != "${start}" ]] ; then - info "rl::canon retry with: ${work}" + info " rl::canon retry with: ${work}" start="${work}" retval=1 continue fi - info "rl::canon invalid path: ${work}" + info " rl::canon invalid path: ${work}" subject="${work}" run=0 && retval=1 fi done - printf -- "${subject}\n" + printf -- "%s\n" "${subject}" return ${retval} } @@ -149,15 +165,15 @@ rl::cleanpath() { local rex_tmp= info "Cleaning path... ${1:-}" - + work="${1:-}" # Remove multiple / while [[ "${work:-}" = *"//"* ]]; do work="${work//'//'/'/'}" done - - info "rl::cleanpath result: ${work}" - printf -- "${work}\n" + + info " rl::cleanpath result: ${work}" + printf -- "%s\n" "${work}" } rl::normalize() { @@ -187,8 +203,8 @@ rl::normalize() { work="${BASH_REMATCH[1]}" fi - info "rl::normalize result: ${work}" - printf -- "${work}\n" + info " rl::normalize result: ${work}" + printf -- "%s\n" "${work}" } rl::main() { @@ -218,31 +234,9 @@ rl::main() { done } -function rl::log () { - local log_level="${1:-${LOG_LEVEL}}" - shift - - # all remaining arguments are to be printed - local log_line="" - - while IFS=$'\n' read -r log_line; do - printf "%s [%9s] %s\n" "$(date -u +"%Y-%m-%d %H:%M:%S UTC")" "${log_level}" "${log_line}" 1>&2 - done <<< "${@:-}" -} -emergency() { rl::log emergency "${@}"; exit 1; } -alarm() { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && rl::log alert "${@}"; true; } -critical() { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && rl::log critical "${@}"; true; } -error() { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && rl::log error "${@}"; true; } -warning() { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && rl::log warning "${@}"; true; } -notice() { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && rl::log notice "${@}"; true; } -info() { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && rl::log info "${@}"; true; } -debug() { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && rl::log debug "${@}"; true; } -msg() { echo "$@"; true; } - - # Provide as function to be called when sourced rdlink() { - rl::rdlink "$@" + rl::main "$@" } ### Check if script is _sourced @@ -251,7 +245,8 @@ _sourced=0 if [ -n "${ZSH_EVAL_CONTEXT:-}" ]; then case ${ZSH_EVAL_CONTEXT:-} in *:file) _sourced=1;; esac elif [ -n "${KSH_VERSION:-}" ]; then - [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && _sourced=1 + [ "$(cd "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" \ + != "$(cd "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ] && _sourced=1 elif [ -n "${BASH_VERSION:-}" ]; then (return 0 2>/dev/null) && _sourced=1 else # All other shells: examine $0 for known shell binary filenames @@ -264,3 +259,5 @@ if (( ! _sourced )); then rl::main "$@" fi +### Script EOF + -- 2.30.2 From 24cd62e72040286e7f89c82d5fd3b696f6075e1b Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Tue, 22 Mar 2022 10:53:23 +0100 Subject: [PATCH 16/18] Introduce build script which extracts script logic and removes debug output --- .gitignore | 1 + build.sh | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ rdlink.sh | 8 +++--- 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100755 build.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2b75d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/release diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..149e8f6 --- /dev/null +++ b/build.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +build::rdlink() { + readonly build_dir="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + readonly rdlinkRelease="${build_dir}/release/rdlink.sh" + readonly color_green='\033[1;32m' + readonly color_red='\033[1;31m' + readonly color_less='\033[0m' + readonly shellcheck_cmd="$(command -v shellcheck)" + readonly fileHeader="#!/usr/bin/env bash + +# rdlink [OPTIONS] +# [OPTION] +# -- : End of options marker +# -* : Other options are ignored +# +# License: GNU GPL V3 or later +# Author: Martin Winkler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +" + local testresult_graphic= + testresult_graphic="${color_green}✔${color_less}" # alt. symbol ✓ + testresult_graphic_fail="${color_red}✗${color_less}" # alt. symbol ≠ + + if ! mkdir -p "$(dirname -- "${rdlinkRelease}")" ; then + printf " [ERROR] Cannot create release directory\n" + return 1 + fi + + printf "# Building rdlink release\n" + + # Write header + printf "%s" "${fileHeader}" > "${rdlinkRelease}" + + printf " Extracting script part and removing debug information..." + # Get essential script content + # * extract script part + # * remove script marker + # * remove debug function calls + # * remove debug options from rdlink + # * replace multiple empty lines with one + #if cat "${build_dir}/rdlink.sh" \ + # | sed -n '/### Script.*/,/### Script EOF/ p' \ + # | sed '/### Script.*/d; /^[[:blank:]]*info/d; /-d\{1,2\}/,/;;/d' \ + # | sed '/^$/N; /^\n$/D' \ + # >> "${rdlinkRelease}" 2>/dev/null ; then + if sed -n '/### Script.*/,/### Script EOF/ p' < "${build_dir}/rdlink.sh" \ + | sed '/### Script.*/d; /^[[:blank:]]*info/d; /-d\{1,2\}/,/;;/d' \ + | sed '/^$/N; /^\n$/D' \ + >> "${rdlinkRelease}" 2>/dev/null ; then + printf "%b" "${testresult_graphic}\n" + else + printf "%b" "${testresult_graphic_fail}\n" + return 1 + fi + + if [[ "${shellcheck_cmd}" ]] ; then + printf "%b\n" " Running shellcheck...\033[1A\033[s\n" + if "${shellcheck_cmd}" "${rdlinkRelease}" ; then + printf "%b" "\033[u${testresult_graphic}\n" + else + return 1 + fi + fi + + chmod +x "${rdlinkRelease}" + return 0 +} + +build::rdlink + diff --git a/rdlink.sh b/rdlink.sh index 4b8eb37..a51b6ed 100755 --- a/rdlink.sh +++ b/rdlink.sh @@ -49,8 +49,7 @@ rl::rdlink() { # Follow multiple symlinks while subject="$(rl::quicklink "${subject}")" ; do - # A link was resolved at least once - : + : # A link was resolved at least once info " rl::rdlink - Link found: $subject" done @@ -129,6 +128,7 @@ rl::canon() { subject="${work}${bname:+"/${bname}"}" info " rl::canon valid parent: ${subject}" + # Special: Succeed with valid element after second run; see special below # e.g. /root/. # * /root is valid but not accessible @@ -165,8 +165,8 @@ rl::cleanpath() { local rex_tmp= info "Cleaning path... ${1:-}" - work="${1:-}" + # Remove multiple / while [[ "${work:-}" = *"//"* ]]; do work="${work//'//'/'/'}" @@ -180,8 +180,8 @@ rl::normalize() { local work= info "Normalizing path... ${1:-}" - work="${1:-}" + # Remove dir/.. sequences. local rex_tmp='[^/][^/]*/\.\./*' while [[ "${work}" =~ $rex_tmp ]] ; do -- 2.30.2 From 3fcfe89c193e0f4503aa31a2cc6a937d6f2f6836 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Tue, 22 Mar 2022 10:54:22 +0100 Subject: [PATCH 17/18] Use released rdlink for testing and build it if missing --- test/test_rdlink.sh | 22 ++++++++++++---------- test/totest.sh | 8 ++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh index baab27b..e9c059a 100755 --- a/test/test_rdlink.sh +++ b/test/test_rdlink.sh @@ -17,8 +17,9 @@ # Internal tests will be executed if no arguments are found readonly test_dir="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -readonly tool_rdlink="${test_dir}/../rdlink.sh" +readonly tool_rdlink="${test_dir}/../release/rdlink.sh" readonly tool_readlink="$(command -v readlink) -f --" +readonly build_cmd="${test_dir}/../build.sh" readonly config_path_width=45 @@ -163,6 +164,8 @@ rl::test() { esac done + [[ ! -e "${tool_rdlink}" ]] && "${build_cmd}" + # Cmd line arguments (( ! flag_onlycompete )) && [[ ! "$@" ]] && toassert_init @@ -183,13 +186,12 @@ rl::test() { # Save first element for string compare firstelement="${path}" - # Check for excludes elif excludemsg="$(toexclude "${firstelement}")"; then + # Current path is excluded (( tests_skipped++ )) rl::printPath "$(( i/2 ))" "${firstelement}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" - # Execute tests else - # Do the compare between two following elements + # Execute test case if ! rl::testcmp "$(( i/2 ))" "${firstelement}" \ "${path}" "$($tool_rdlink -- "${firstelement}")"; then (( tests_failed++ )) @@ -206,16 +208,16 @@ rl::test() { (( testend )) && break done (( testend )) && rl::printTestSummary $tests_success $tests_failed $tests_skipped && return 1 - + # Initialize competition tests (( ! flag_onlyassert )) && tocompete_init - + # Only run `compete_args` if arguments are available if [[ "$@" ]]; then compete_args=( "Tests from command line" "$@" ) tocompete=(compete_args) fi - + # Compare output of rdlink and readlink -f if (( ${#tocompete[@]} )) ; then printf "\n# Competition tests (readlink -f == rdlink)\n" @@ -231,13 +233,13 @@ rl::test() { (( ! flag_extendedOutput )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" else - # Check for excludes if excludemsg="$(toexclude "${path}")" ; then + # Current path is excluded (( tests_skipped++ )) rl::printPath "${i}" "${path}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" - # Execute tests elif ! rl::testcmp "${i}" "${path}" \ "$(${tool_readlink} "$path")" "$(${tool_rdlink} -- "$path")"; then + # Test case failed (( tests_failed++ )) # Run all tests if option -a is pressend (( ! $flag_runall )) && testend=1 && break @@ -250,7 +252,7 @@ rl::test() { done (( testend )) && break done - + rl::printTestSummary $tests_success $tests_failed $tests_skipped (( testend )) && return 1 || return 0 } diff --git a/test/totest.sh b/test/totest.sh index d6fe2b8..f26be7a 100644 --- a/test/totest.sh +++ b/test/totest.sh @@ -30,7 +30,7 @@ compete_relative=( "Resolving of relative links" # Valid "dir_1/file" # Valid relative link to path with relative element ) - + compete_all=( "Test - everything starting from /" /**/* ) @@ -51,7 +51,7 @@ tocompete_init() { touch "dir_2/dir_22/file" ln -s "dir_2/dir_22" "dir_3_l" ln -s "../dir_3_l/file" "dir_1/file" - + # compete_no_permission mkdir -p "${_tc_tmp}/noperm" chmod 400 "${_tc_tmp}/noperm" @@ -62,11 +62,11 @@ tocompete_init() { # compete_relative ln -s "../nofile" "${_tc_tmp}/nofile_l" ln -s "../nodir/nofile" "${_tc_tmp}/nopath_l" - + #echo "rl: " && readlink "${_tc_tmp}/lnoperm" #ls -l "${_tc_tmp}" } - + # Base directory for the test cd "${_tc_tmp}" -- 2.30.2 From ad0f7bd646607f7969aa202e1b5404ed1d200619 Mon Sep 17 00:00:00 2001 From: Martin Winkler Date: Tue, 22 Mar 2022 12:00:55 +0100 Subject: [PATCH 18/18] Adding dependencies and known issues chapter --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d8adc0..af886e0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ # readlink -Replicate `readlink -f` as bash script using mainly dependencies available on most bash 3 platforms. \ No newline at end of file +Replicate `readlink -f` as bash script using mainly dependencies available on most platforms: + +* `readlink` (without options) +* `cd` +* `pwd -P` +* `test -e` (as `[ -e ]`) + +# Known issues + +* When `readlink` is part of a pipe some links contain the current pipe described like `pipe:[123456]` where the number changes on every call. + `readlink` resolves these as valid links. + + e.g. + ``` + $ readlink -f /dev/stdout + /dev/pts/0 + + $ readlink -f /dev/stdout | cat + /proc/737861/fd/pipe:[2332393] + + $ ./rdlink.sh /dev/stdout + + ``` + Whereas `rdlink` is not able to evaluate such links, because the pipe is already invalid as soon as a expression like `result="$(readlink "${tocheck}")"` returns. \ No newline at end of file -- 2.30.2