Permalink
Browse files

Figuring out call stack introspection in bash via spec tests.

It appears the variables that collectively store the call stack don't
line up!  ${BASH_SOURCE[i]} and ${BASH_LINENO[i]} are out of whack!

But the current position is ${BASH_SOURCE[0]} and $LINENO.  Sigh.

This was good to test out, but I don't think Oil will compatible with
bash (and hence debuggers like bashdb).

Also:
- Test the values of $$ and $BASHPID in command subs too.
  • Loading branch information...
Andy Chu
Andy Chu committed Dec 29, 2017
1 parent f9a5986 commit cf950494f84a3df58596783609ad5945cc7c66da
Showing with 142 additions and 16 deletions.
  1. +3 −3 core/alloc.py
  2. +1 −1 core/cmd_exec.py
  3. +74 −4 spec/introspect.test.sh
  4. +36 −8 spec/special-vars.test.sh
  5. +7 −0 spec/testdata/bash-source-2.sh
  6. +21 −0 spec/testdata/bash-source.sh
View
@@ -116,16 +116,16 @@ def GetDebugInfo(self, line_id):
def CompletionArena(pool):
"""A temporary arena that only exists for a function call?"""
arena = pool.NewArena()
arena.PushSource('<completion>')
arena.PushSource('<temp completion buffer>')
return arena
def PluginArena():
def PluginArena(source_name):
"""For PS4, etc."""
# TODO: Should there only be one pool? This isn't worked out yet.
pool = Pool()
arena = pool.NewArena()
arena.PushSource('<plugin>')
arena.PushSource(source_name)
return arena
View
@@ -1115,7 +1115,7 @@ def __init__(self, exec_opts, mem, word_ev):
self.mem = mem
self.word_ev = word_ev
self.arena = alloc.PluginArena()
self.arena = alloc.PluginArena('<$PS4>')
self.parse_cache = {} # PS4 value -> CompoundWord. PS4 is scoped.
def OnSimpleCommand(self, argv):
View
@@ -1,16 +1,86 @@
#!/usr/bin/env bash
#
# TODO:
# BASH_SOURCE, BASH_LINENO, caller builtin
# Test call stack introspection. There are a bunch of special variables
# defined here:
#
# https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html
#
# - The shell function ${FUNCNAME[$i]} is defined in the file
# ${BASH_SOURCE[$i]} and called from ${BASH_SOURCE[$i+1]}
#
# - ${BASH_LINENO[$i]} is the line number in the source file
# (${BASH_SOURCE[$i+1]}) where ${FUNCNAME[$i]} was called (or
# ${BASH_LINENO[$i-1]} if referenced within another shell function).
#
# - For instance, ${FUNCNAME[$i]} was called from the file
# ${BASH_SOURCE[$i+1]} at line number ${BASH_LINENO[$i]}. The caller builtin
# displays the current call stack using this information.
#
# So ${BASH_SOURCE[@]} doesn't line up with ${BASH_LINENO}. But
# ${BASH_SOURCE[0]} does line up with $LINENO!
#
# Geez.
### ${FUNCNAME[@]} array
g() {
argv.py "${FUNCNAME[@]}"
}
f() {
argv.py "${FUNCNAME[@]}"
g
argv.py "${FUNCNAME[@]}"
}
f
## STDOUT:
['f']
['g', 'f']
['f']
## END
### ${BASH_SOURCE[@]} is a stack of source files for function calls
$SH spec/testdata/bash-source.sh
## STDOUT:
['begin F funcs', 'f', 'main']
['begin F files', 'spec/testdata/bash-source.sh', 'spec/testdata/bash-source.sh']
['begin F lines', '21', '0']
['G funcs', 'g', 'f', 'main']
['G files', 'spec/testdata/bash-source-2.sh', 'spec/testdata/bash-source.sh', 'spec/testdata/bash-source.sh']
['G lines', '15', '21', '0']
['end F funcs', 'f', 'main']
['end F', 'spec/testdata/bash-source.sh', 'spec/testdata/bash-source.sh']
['end F lines', '21', '0']
## END
### ${BASH_LINENO[@]} is a stack of line numbers for function calls
# note: it's CALLS, not DEFINITIONS.
g() {
argv.py "${FUNCNAME[@]}"
argv.py G "${BASH_LINENO[@]}"
}
f() {
argv.py 'begin F' "${BASH_LINENO[@]}"
g # line 6
argv.py 'end F' "${BASH_LINENO[@]}"
}
f # line 9
## STDOUT:
['begin F', '9']
['G', '6', '9']
['end F', '9']
## END
### $LINENO is the current line, not line of function call
g() {
argv.py $LINENO # line 2
}
f() {
argv.py $LINENO # line 5
g
argv.py $LINENO # line 7
}
f
# stdout-json: "['f']\n['g', 'f']\n['f']\n"
## STDOUT:
['5']
['2']
['7']
## END
View
@@ -3,6 +3,14 @@
# NOTE:
# - $! is tested in background.test.sh
# - $- is tested in sh-options
#
# TODO: It would be nice to make a table, like:
#
# $$ $BASHPID $PPID $SHLVL $BASH_SUBSHELL
# X
# (Subshell, Command Sub, Pipeline, Spawn $0)
#
# And see whether the variable changed.
### $PWD
# Just test that it has a slash for now.
@@ -39,7 +47,7 @@ echo $_
echo $$ | egrep '[0-9]+'
# status: 0
### $$ doesn't change with subshell
### $$ doesn't change with subshell or command sub
# Just test that it has decimal digits
set -o errexit
die() {
@@ -48,28 +56,48 @@ die() {
parent=$$
test -n "$parent" || die "empty PID in parent"
( child=$$
test -n "$child" || die "empty PID in child"
test -n "$child" || die "empty PID in subshell"
test "$parent" = "$child" || die "should be equal: $parent != $child"
echo 'subshell OK'
)
echo $( child=$$
test -n "$child" || die "empty PID in command sub"
test "$parent" = "$child" || die "should be equal: $parent != $child"
echo 'command sub OK'
)
exit 3 # make sure we got here
# stdout-json: ""
# status: 3
## STDOUT:
subshell OK
command sub OK
## END
### $BASHPID DOES change with subshell
### $BASHPID DOES change with subshell and command sub
set -o errexit
die() {
echo 1>&2 "$@"; exit 1
}
parent=$BASHPID
test -n "$parent" || die "empty BASHPID in parent"
( child=$BASHPID
test -n "$child" || die "empty BASHPID in child"
test -n "$child" || die "empty BASHPID in subshell"
test "$parent" != "$child" || die "should not be equal: $parent = $child"
echo 'subshell OK'
)
echo $( child=$BASHPID
test -n "$child" || die "empty BASHPID in command sub"
test "$parent" != "$child" ||
die "should not be equal: $parent = $child"
echo 'command sub OK'
)
exit 3 # make sure we got here
# stdout-json: ""
# status: 3
# N-I dash status: 1
## status: 3
## STDOUT:
subshell OK
command sub OK
## END
## N-I dash status: 1
## N-I dash stdout-json: ""
### Background PID $! looks like a PID
sleep 0.01 &
@@ -0,0 +1,7 @@
#!/bin/bash
g() {
argv 'G funcs' "${FUNCNAME[@]}"
argv 'G files' "${BASH_SOURCE[@]}"
argv 'G lines' "${BASH_LINENO[@]}"
}
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
#
# For spec/introspect.test.sh.
source spec/testdata/bash-source-2.sh
argv() {
spec/bin/argv.py "$@"
}
f() {
argv 'begin F funcs' "${FUNCNAME[@]}"
argv 'begin F files' "${BASH_SOURCE[@]}"
argv 'begin F lines' "${BASH_LINENO[@]}"
g
argv 'end F funcs' "${FUNCNAME[@]}"
argv 'end F' "${BASH_SOURCE[@]}"
argv 'end F lines' "${BASH_LINENO[@]}"
}
f # call a function it defines

0 comments on commit cf95049

Please sign in to comment.