name: "Call stack" common_test_files: - path: "callstack_helpers.sh" contents: | # Helper to dump frame-independent call stack state. # Optionally takes a prefix string to use in all output. dump_source_info() { local prefix="${1:-}" # Normalize shell name to make sure it matches. # TODO: Once all tests are run on 5.3, this will need to be revisited. echo "${prefix} caller: $(caller 2>&1 || echo '')" | sed -e "s|$0|main|g" | sed -e "s/environment/main/g" echo "${prefix} LINENO: ${LINENO}" echo "${prefix} FUNCNAME[@]: ${FUNCNAME[*]}" # Normalize shell name to make sure it matches. # TODO: Once all tests are run on 5.3, this will need to be revisited. echo "${prefix} BASH_SOURCE[@]: ${BASH_SOURCE[*]}" | sed -e "s|$0|main|g" | sed -e "s/environment/main/g" # TODO(BASH_LINENO): implement correct BASH_LINENO tracking # echo "${prefix} BASH_LINENO[@]: ${BASH_LINENO[*]}" } # Helper to dump frame-specific call stack state dump_frame() { local prefix="${1:-}" local frame="${2:-0}" echo "${prefix}[Frame $frame]" # Normalize shell name to make sure it matches. # TODO: Once all tests are run on 5.3, this will need to be revisited. echo "${prefix} caller N: $(caller $frame 2>&1 || echo '')" | sed -e "s|$0|main|g" | sed -e "s/environment/main/g" } # Helper to dump all stack info dump_stack() { local prefix="${1:-}" dump_source_info "$prefix" local max_frames="${2:-20}" local i for ((i=0; i/dev/null 2>&1; then break fi dump_frame "$prefix" $i done } cases: - name: "No calls - top level" stdin: | source callstack_helpers.sh echo "=== Top level ===" dump_stack - name: "Simple function call" stdin: | source callstack_helpers.sh my_function() { echo "=== In my_function ===" dump_stack " " } my_function - name: "Nested function calls" stdin: | source callstack_helpers.sh level3() { echo "=== In level3 ===" dump_stack " " } level2() { echo "=== In level2 ===" dump_stack " " level3 } level1() { echo "=== In level1 ===" dump_stack " " level2 } level1 - name: "Function call from sourced script" test_files: - path: "sourced.sh" contents: | sourced_function() { echo "=== In sourced_function ===" dump_stack " " } stdin: | source callstack_helpers.sh source sourced.sh sourced_function - name: "Nested sourcing" test_files: - path: "outer.sh" contents: | echo "=== In outer.sh top level ===" dump_stack " " source inner.sh - path: "inner.sh" contents: | echo "=== In inner.sh top level ===" dump_stack " " inner_func() { echo "=== In inner_func ===" dump_stack " " } inner_func stdin: | source callstack_helpers.sh command source outer.sh - name: "Command substitution in top level" stdin: | source callstack_helpers.sh result=$(dump_stack " [CmdSub] ") echo "$result" - name: "Command substitution in function" stdin: | source callstack_helpers.sh my_function() { echo "=== In my_function ===" result=$(dump_stack " [CmdSub] ") echo "$result" } my_function - name: "Command substitution invoking function" stdin: | source callstack_helpers.sh my_function() { echo "=== In my_function via cmdSub ===" dump_stack " " } result=$(my_function) echo "$result" - name: "Nested command substitutions" stdin: | source callstack_helpers.sh inner_func() { echo "=== In inner_func ===" dump_stack " " } outer_func() { echo "=== In outer_func ===" result=$(inner_func) echo "$result" } final=$(outer_func) echo "$final" - name: "eval at top level" stdin: | source callstack_helpers.sh eval 'echo "=== In eval ==="; dump_stack " "' - name: "eval in function" stdin: | source callstack_helpers.sh my_function() { echo "=== In my_function ===" eval 'echo "=== In eval ==="; dump_stack " "' } my_function - name: "eval calling function" stdin: | source callstack_helpers.sh my_function() { echo "=== In my_function via eval ===" dump_stack " " } eval 'my_function' - name: "Nested evals" stdin: | source callstack_helpers.sh my_function() { echo "=== In my_function ===" dump_stack " " } eval 'eval "my_function"' - name: "eval with command substitution" stdin: | source callstack_helpers.sh my_function() { echo "=== In my_function ===" dump_stack " " } result=$(eval 'my_function') echo "$result" - name: "Trap handler - DEBUG" stdin: | source callstack_helpers.sh trap_handler() { echo "=== In DEBUG trap handler ===" dump_stack " " } trap 'trap_handler' DEBUG echo "Command 1" echo "Command 2" - name: "Trap handler - DEBUG in function" stdin: | source callstack_helpers.sh trap_handler() { echo "=== In DEBUG trap handler ===" dump_stack " " } trap 'trap_handler' DEBUG my_function() { echo "In my_function" echo "Still in my_function" } my_function - name: "Trap handler - EXIT" stdin: | source callstack_helpers.sh trap 'echo "=== In EXIT trap ==="; dump_stack " "' EXIT echo "Main script" - name: "Trap handler calling function" stdin: | source callstack_helpers.sh trap_function() { echo "=== In trap_function ===" dump_stack " " } trap 'trap_function' EXIT echo "Main script" - name: "LINENO tracking across contexts" stdin: | echo "Line 1: LINENO=${LINENO}" echo "Line 2: LINENO=${LINENO}" my_function() { echo "In function line 1: LINENO=${LINENO}" echo "In function line 2: LINENO=${LINENO}" } my_function echo "Line 3: LINENO=${LINENO}" - name: "LINENO in eval" stdin: | echo "Before eval: LINENO=${LINENO}" eval 'echo "In eval: LINENO=${LINENO}"' echo "After eval: LINENO=${LINENO}" - name: "LINENO in command substitution" stdin: | echo "Before cmdSub: LINENO=${LINENO}" result=$(echo "In cmdSub: LINENO=${LINENO}") echo "$result" echo "After cmdSub: LINENO=${LINENO}" - name: "LINENO in sourced script" test_files: - path: "script.sh" contents: | echo "Sourced line 1: LINENO=${LINENO}" echo "Sourced line 2: LINENO=${LINENO}" sourced_func() { echo "In sourced_func: LINENO=${LINENO}" } sourced_func stdin: | echo "Main line 1: LINENO=${LINENO}" source script.sh echo "Main line 2: LINENO=${LINENO}" - name: "different depths" stdin: | source callstack_helpers.sh level3() { dump_stack } level2() { level3 } level1() { level2 } level1 - name: "BASH_SOURCE and FUNCNAME arrays" test_files: - path: "outer.sh" contents: | outer_func() { echo "=== In outer_func ===" echo " FUNCNAME[@]: ${FUNCNAME[*]}" echo " BASH_SOURCE[@]: ${BASH_SOURCE[*]}" source inner.sh } - path: "inner.sh" contents: | inner_func() { echo "=== In inner_func ===" echo " FUNCNAME[@]: ${FUNCNAME[*]}" echo " BASH_SOURCE[@]: ${BASH_SOURCE[*]}" } inner_func stdin: | source outer.sh outer_func - name: "Mixed: eval in sourced function with command substitution" test_files: - path: "script.sh" contents: | script_func() { echo "=== In script_func ===" result=$(eval 'dump_stack " [Eval->CmdSub] "') echo "$result" } stdin: | source callstack_helpers.sh source script.sh script_func - name: "Complex nesting: function -> eval -> cmdSub -> function" stdin: | source callstack_helpers.sh inner_func() { echo "=== In inner_func ===" dump_stack " " } outer_func() { echo "=== In outer_func ===" eval 'result=$(inner_func); echo "$result"' } outer_func # Tests with -c flag - name: "Call stack with -c flag" args: ["-c", "source callstack_helpers.sh; echo '=== Via -c ==='; dump_stack"] - name: "Function call with -c flag" args: [ "-c", "my_func() { echo '=== In my_func via -c ==='; dump_stack; }; source callstack_helpers.sh; my_func", ] - name: "Nested functions with -c flag" args: [ "-c", "f2() { echo 'In f2'; dump_stack; }; f1() { echo 'In f1'; f2; }; source callstack_helpers.sh; f1", ] - name: "eval with -c flag" args: [ "-c", "eval 'source callstack_helpers.sh; echo In eval via -c; dump_stack'", ] - name: "Command substitution with -c flag" args: [ "-c", "source callstack_helpers.sh; result=$(echo LINENO=${LINENO}; dump_stack); echo $result", ] # Interactive mode tests - name: "Interactive mode - simple function" known_failure: true # TODO(LINENO): implement accurate interactive mode tracking args: ["-i"] ignore_stderr: true stdin: | source callstack_helpers.sh f() { dump_stack; } f - name: "Interactive mode - FUNCNAME and BASH_SOURCE" args: ["-i"] ignore_stderr: true stdin: | f() { echo "FUNCNAME: ${FUNCNAME[@]}"; echo "BASH_SOURCE: ${BASH_SOURCE[@]}"; } f # PROMPT_COMMAND tests - name: "PROMPT_COMMAND with call stack" known_failure: true # TODO(LINENO): implement accurate PROMPT_COMMAND line tracking args: ["-i"] ignore_stderr: true stdin: | source callstack_helpers.sh PROMPT_COMMAND='dump_stack "[PC] "' echo test - name: "PROMPT_COMMAND calling function" known_failure: true # TODO(LINENO): implement accurate PROMPT_COMMAND line tracking args: ["-i"] ignore_stderr: true stdin: | source callstack_helpers.sh pc_func() { echo "[PC_FUNC] In function"; dump_stack; } PROMPT_COMMAND='pc_func' echo test - name: "PROMPT_COMMAND with nested calls" known_failure: true # TODO(LINENO): implement accurate PROMPT_COMMAND line tracking args: ["-i"] ignore_stderr: true stdin: | source callstack_helpers.sh inner() { dump_stack; } outer() { inner; } PROMPT_COMMAND='outer' echo test # Additional edge cases - name: "Recursive function call stack" stdin: | source callstack_helpers.sh recursive_func() { local depth=$1 echo "=== Depth $depth ===" if [ $depth -gt 0 ]; then recursive_func $((depth - 1)) else dump_stack " " fi } recursive_func 3 - name: "Call stack with process substitution" stdin: | source callstack_helpers.sh my_func() { echo "=== In my_func ===" dump_stack " " } # Process substitution context cat <(my_func) - name: "Call stack across pipe" stdin: | source callstack_helpers.sh my_func() { echo "=== In my_func ===" dump_stack " " } my_func | cat - name: "Call stack in subshell" stdin: | source callstack_helpers.sh my_func() { echo "=== In my_func (subshell) ===" dump_stack " " } (my_func) - name: "Call stack with background job" stdin: | source callstack_helpers.sh my_func() { echo "=== In my_func (background) ===" dump_stack " " } my_func & wait - name: "BASH_SOURCE indexing" test_files: - path: "script.sh" contents: | echo "In script.sh" echo " BASH_SOURCE[0]: ${BASH_SOURCE[0]}" echo " BASH_SOURCE[1]: ${BASH_SOURCE[1]}" script_func() { echo "In script_func" echo " BASH_SOURCE[0]: ${BASH_SOURCE[0]}" echo " BASH_SOURCE[1]: ${BASH_SOURCE[1]}" echo " BASH_SOURCE[2]: ${BASH_SOURCE[2]}" } script_func stdin: | echo "Main script" echo " BASH_SOURCE[0]: ${BASH_SOURCE[0]}" source script.sh - name: "FUNCNAME at top level" stdin: | echo "Top level FUNCNAME: '${FUNCNAME}'" echo "Top level FUNCNAME[@]: '${FUNCNAME[@]}'" echo "Top level FUNCNAME[*]: '${FUNCNAME[*]}'" - name: "Empty BASH_LINENO at top level" stdin: | echo "Top level BASH_LINENO[@]: '${BASH_LINENO[@]}'" echo "Top level BASH_LINENO[*]: '${BASH_LINENO[*]}'" - name: "Call stack in trap with function call" stdin: | source callstack_helpers.sh trap_func() { echo "=== In trap_func ===" dump_stack " " } my_func() { echo "=== In my_func ===" echo "About to trigger trap" } trap 'trap_func' DEBUG my_func - name: "Call stack: source within eval within function" test_files: - path: "script.sh" contents: | echo "=== In sourced script ===" dump_stack " " stdin: | source callstack_helpers.sh my_func() { echo "=== In my_func ===" eval 'source script.sh' } my_func - name: "Multiple eval levels" stdin: | source callstack_helpers.sh my_func() { echo "=== In my_func ===" dump_stack " " } eval 'eval "eval \"my_func\""' - name: "LINENO persistence in function" stdin: | my_func() { local line1=${LINENO} local line2=${LINENO} local line3=${LINENO} echo "line1: $line1" echo "line2: $line2" echo "line3: $line3" } my_func