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/.gitignore b/.gitignore new file mode 100644 index 0000000..f2b75d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/release 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 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 new file mode 100755 index 0000000..a51b6ed --- /dev/null +++ b/rdlink.sh @@ -0,0 +1,263 @@ +#!/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 + +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= + + info "Processing: $*" + info " with pwd: $(pwd)" + + subject="$(rl::cleanpath "${1:-}")" || true + + # Follow multiple symlinks + while subject="$(rl::quicklink "${subject}")" ; do + : # A link was resolved at least once + info " rl::rdlink - Link found: $subject" + 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 + info " rl::rdlink exit - invalid path ${work}" + printf "\n" + return 1 + else + subject="${work}" + fi + + printf "%s\n" "${subject}" +} + +rl::quicklink() { + subject= + work= + + info "Quicklink... ${1:-}" + + # Check if current candidate is a symlink + if ! subject=$(readlink -- "${1:-}"); then + printf -- "%s\n" "${1:-}" + return 1 + fi + 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 " rl::quicklink relative link resolved: ${subject}" + fi + + printf "%s\n" "${subject}" + return 0 +} + +rl::canon() { + local subject= + local work= + local bname= + local run=1 + local start= + local retval=0 + + start="${1:-}" + info "Canonicalize path... ${start}" + + 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}")" + + # 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 -- "%s\n" "${subject}" + return ${retval} +} + +rl::cleanpath() { + local work= + local rex_tmp= + + info "Cleaning path... ${1:-}" + work="${1:-}" + + # Remove multiple / + while [[ "${work:-}" = *"//"* ]]; do + work="${work//'//'/'/'}" + done + + info " rl::cleanpath result: ${work}" + printf -- "%s\n" "${work}" +} + +rl::normalize() { + local work= + + info "Normalizing path... ${1:-}" + work="${1:-}" + + # Remove dir/.. sequences. + local rex_tmp='[^/][^/]*/\.\./*' + while [[ "${work}" =~ $rex_tmp ]] ; do + work="${work/"${BASH_REMATCH[0]}"/}" + done + + # Remove /./ and /.$ 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 -- "%s\n" "${work}" +} + +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 +} + +# 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 + +### Script EOF + diff --git a/test/test_rdlink.sh b/test/test_rdlink.sh new file mode 100755 index 0000000..e9c059a --- /dev/null +++ b/test/test_rdlink.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash + +# Automatic tests for rdlink +# Two kinds of tests are supported +# * `tocompete` : competition between readlink and rdlink +# * `toassert` : assert expected path and input path + +# test_rdlink.sh [OPTIONS] [PATH TO COMPETE] +# [OPTIONS] +# -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}/../release/rdlink.sh" +readonly tool_readlink="$(command -v readlink) -f --" +readonly build_cmd="${test_dir}/../build.sh" + +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_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:-"-"}" + 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 + 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%${inputwidth}s" "${truncate_graphic}" "${input}" + fi +} + +rl::printTestSummary() { + local success=${1:-0} + local failed=${2:-0} + local skipped=${3:-0} + local total=$(( success + failed + skipped )) + readonly columnwidth=7 + readonly tableformat="%${columnwidth}s | %${columnwidth}s | %${columnwidth}s | %${columnwidth}s\n" + + printf "\n# Result : ${tableformat}" "success" "failed" "skipped" "total" + printf " ${tableformat}" "${success}" "${failed}" "${skipped}" "${total}" +} + +# rl::testcmp +# Compare results and print summary +rl::testcmp() { + 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 ≠ + + 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 + rl::printPath ${testnum} "${input}" + printf " %b\n" "${testresult_graphic}" + 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 + return 1 + fi + return 0 + fi + + # Change result graphic if test failed + (( ! testresult )) && testresult_graphic="→" # "${color_green}→${color_less}" + 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}" + return 1 + fi + return 0 +} + +rl::test() { + local i=0 + local path= + local arraywalker= + local firstelement= + 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 + --) ## End of options + shift && break ;; + -a|--run-all) ## Run all even if tests fail + flag_runall=1 + shift ;; + -x|--extended-output) + flag_extendedOutput=1 + shift ;; + -xd|--verbose) + flag_extendedOutput=1 + flag_verbose=1 + shift ;; + -oa|--only-assert) + flag_onlyassert=1 + shift ;; + -oc|--only-compete) + flag_onlycompete=1 + shift ;; + -e|--error) + flag_printerror=1 + shift ;; + -*|--*) + printf "Invalid argument\n" + exit 1 ;; + esac + done + + [[ ! -e "${tool_rdlink}" ]] && "${build_cmd}" + + # Cmd line arguments + (( ! flag_onlycompete )) && [[ ! "$@" ]] && toassert_init + + # Compare against expected result + if (( ${#toassert[@]} )) ; then + printf "\n# Assertion tests (\"Expected path\" == rdlink)\n" + fi + for testarray in "${toassert[@]}"; do + i=0 + arraywalker="$testarray"[@] + for path in "${!arraywalker}"; do + if (( ! i )); then + # Print title in array element 0 + printf " ### %b ###\n" "${color_yellow}${path}${color_less}" + (( ! flag_extendedOutput )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" + + elif [ -z "$firstelement" ]; then + # Save first element for string compare + firstelement="${path}" + + elif excludemsg="$(toexclude "${firstelement}")"; then + # Current path is excluded + (( tests_skipped++ )) + rl::printPath "$(( i/2 ))" "${firstelement}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" + else + # Execute test case + 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 )) && 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" + fi + for testarray in "${tocompete[@]}"; do + i=0 + arraywalker="$testarray"[@] + for path in "${!arraywalker}"; do + + if (( ! i )); then + # Print title in array element 0 + printf " ### %b ###\n" "${color_yellow}${path}${color_less}" + (( ! flag_extendedOutput )) && printf "%${config_path_width}s %s %s\n" "" "" "✗ [ACTUAL]" + + else + if excludemsg="$(toexclude "${path}")" ; then + # Current path is excluded + (( tests_skipped++ )) + rl::printPath "${i}" "${path}" && printf " %b skip (%s)\n" "🛇" "${excludemsg}" + 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 + else + (( tests_success++ )) + fi + fi + + ((i++)) + done + (( testend )) && break + done + + rl::printTestSummary $tests_success $tests_failed $tests_skipped + (( testend )) && return 1 || return 0 +} + +#time rl::test "$@" +rl::test "$@" diff --git a/test/totest.sh b/test/totest.sh new file mode 100644 index 0000000..f26be7a --- /dev/null +++ b/test/totest.sh @@ -0,0 +1,207 @@ +#!/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 test suites (arrays) +compete_canonicalize=( "Canonicalize invalid path" + "///tmp//./b" + "//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 /" + /**/* +) + + +tocompete_init() { + # initialize custom test structure + { + 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" + chmod 400 "${_tc_tmp}/noperm" + 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 + compete_invalid=( "Invalid files with and without valid path" + "${_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" + #"/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)" + "noperm"* + "lroot"* + "/" + "/root" + "/root/" + "/root/." + "/root/.." + "/proc/"**/cwd # special - no permission for links + ) + + # Add tests to global test array from test_rdlink + tocompete+=( + compete_canonicalize + compete_invalid + compete_relative + compete_no_permission + compete_links + compete_all + ) +} + +tocompete_clean() { + rm -rf "${_tc_tmp}" +} + +########## Assertion Block ########## + +# ta = test assert +_ta_tmp="${test_dir:-"/tmp"}/tmp_assert" + +# Assertion string compare test arrays +# +# Array format +# [0] = Title +# [1] input == [2] expected +# [3] input == [4] expected +# ... + +toassert_init() { + { + 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}/miss c" "${_ta_tmp}/miss c" + "rel_a" "${_ta_tmp}/rel_a" + "../rel_b" "$(cd ".." && pwd)/rel_b" + ) + + # Add test arrays to global test array from test_rdlink + toassert+=( + assert_invalid_files + ) +} + +toassert_clean() { + rm -rf "${_ta_tmp}" +} + +########## Common data and functions ########## +# +# [0] exclude regex [1] Reason for exclution +# [2] exclude regex [3] Reason for exclution +exclude_path=( + "/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() { + local path= + local exclude= + for path in "${exclude_path[@]}" ; do + if [[ ! "${exclude}" ]] ; then + exclude="${path}" + else + # return reason for exclution + if [[ "${1:-}" =~ ${exclude} ]] ; then + printf "${path}" + return 0 + fi + exclude= + fi + done + return 1 +} + +########## Clean custom test data ########## +# +totest_cleanall() { + tocompete_clean + toassert_clean +} +trap totest_cleanall EXIT +