Skip to content

Commit

Permalink
Implement BASH_LINENO, and attempt to implement BASH_SOURCE.
Browse files Browse the repository at this point in the history
BASH_SOURCE is for the definition site, not the call site.

- Detect exit code 124 for completion function
- Better tracing.

Also fix some spec test failures.  Still one more.
  • Loading branch information
Andy Chu committed Sep 24, 2018
1 parent ca71604 commit 42b4f96
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 39 deletions.
16 changes: 10 additions & 6 deletions core/cmd_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def __init__(self, mem, fd_state, funcs, comp_lookup, exec_opts, parse_ctx,
self.arena = parse_ctx.arena
self.aliases = parse_ctx.aliases # alias name -> string
self.dumper = dumper
self.debug_f = debug_f # Used by ShellFuncAction too

self.splitter = legacy.SplitContext(self.mem)
self.word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, self.splitter,
Expand All @@ -148,8 +149,7 @@ def __init__(self, mem, fd_state, funcs, comp_lookup, exec_opts, parse_ctx,
self.job_state = process.JobState()

self.loop_level = 0 # for detecting bad top-level break/continue

if 1:
if 0:
trace_f = debug_f
else:
trace_f = util.DebugFile(sys.stderr)
Expand Down Expand Up @@ -808,7 +808,7 @@ def _Dispatch(self, node, fork_external):
else:
current_spid = const.NO_INTEGER
self.mem.SetCurrentSpanId(current_spid)
self.tracer.OnAssignment(lval, val, flags, lookup_mode)
self.tracer.OnAssignment(lval, pair.op, val, flags, lookup_mode)

# PATCH to be compatible with existing shells: If the assignment had a
# command sub like:
Expand Down Expand Up @@ -1395,12 +1395,15 @@ def _RunFunc(self, func_node, argv):

def RunFuncForCompletion(self, func_node):
try:
self._RunFunc(func_node, [])
status = self._RunFunc(func_node, [])
except util.FatalRuntimeError as e:
ui.PrettyPrintError(e, self.arena, sys.stderr)
status = e.exit_status if e.exit_status is not None else 1
except _ControlFlow as e:
# shouldn't be able to exit the shell from a completion hook!
util.error('Attempted to exit from completion hook.')
status = 1
return status


class Tracer(object):
Expand Down Expand Up @@ -1486,14 +1489,15 @@ def OnSimpleCommand(self, argv):
cmd = ' '.join(pretty.Str(a) for a in argv)
self.f.log('%s%s%s', first_char, prefix, cmd)

def OnAssignment(self, lval, val, flags, lookup_mode):
def OnAssignment(self, lval, op, val, flags, lookup_mode):
# NOTE: I think tracing should be on by default? For post-mortem viewing.
if not self.exec_opts.xtrace:
return

# Now we have to get the prefix
first_char, prefix = self._EvalPS4()
self.f.log('%s%s%s = %s', first_char, prefix, lval, val)
op_str = {assign_op_e.Equal: '=', assign_op_e.PlusEqual: '+='}[op]
self.f.log('%s%s%s %s %s', first_char, prefix, lval, op_str, val)

def Event(self):
"""
Expand Down
45 changes: 19 additions & 26 deletions core/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,49 +201,42 @@ def __repr__(self):
# TODO: Add file and line number here!
return '<ShellFuncAction %r>' % (self.func.name,)

def Matches(self, words, index, to_complete):
# TODO:
# - Set COMP_CWORD etc. in ex.mem -- in the global namespace I guess
# - Then parse the reply here

# This is like a stack code:
# for word in words:
# self.ex.PushString(word)
# self.ex.PushString('COMP_WORDS')
# self.ex.MakeArray()

# self.ex.PushString(str(index))
# self.ex.PushString('COMP_CWORD')

# TODO: Get the name instead!
# self.ex.PushString(self.func_name)
# self.ex.Call() # call wit no arguments

# self.ex.PushString('COMP_REPLY')

# How does this one work?
# reply = []
# self.ex.GetArray(reply)
def log(self, *args):
self.ex.debug_f.log(*args)

def Matches(self, words, index, to_complete):
state.SetGlobalArray(self.ex.mem, 'COMP_WORDS', words)
state.SetGlobalString(self.ex.mem, 'COMP_CWORD', str(index))

self.log('Running completion function %r', self.func.name)

# TODO: Delete COMPREPLY here? It doesn't seem to be defined in bash by
# default.

# TODO: We could catch FatalRuntimeError here instead of in RootCompleter?
# But we don't have the arena available.
self.ex.RunFuncForCompletion(self.func)
status = self.ex.RunFuncForCompletion(self.func)
if status == 124:
self.log('Got status 124 from %r', self.func.name)
# The previous run may have registered another function via 'complete',
# i.e. by sourcing a file. Try it again.
status = self.ex.RunFuncForCompletion(self.func)
if status == 124:
util.warn('Got exit code 124 from function %r twice', self.func.name)

# Should be COMP_REPLY to follow naming convention! Lame.
val = state.GetGlobal(self.ex.mem, 'COMPREPLY')
if val.tag == value_e.Undef:
log('COMPREPLY not defined')
util.error('Ran function %s but COMPREPLY was not defined', self.func.name)
return

if val.tag != value_e.StrArray:
log('ERROR: COMPREPLY should be an array, got %s', val)
return
reply = val.strs

print('REPLY', reply)
self.log('COMPREPLY %s', reply)

#reply = ['g1', 'g2', 'h1', 'i1']
for name in sorted(reply):
if name.startswith(to_complete):
Expand Down
34 changes: 32 additions & 2 deletions core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ def _InitVarsFromEnv(self, environ):
SetGlobalString(self, 'HOME', home_dir)

def SetCurrentSpanId(self, span_id):
"""Set the current source location, for BASH_SOURCE< BASH_LINENO, LINENO,
"""Set the current source location, for BASH_SOURCE, BASH_LINENO, LINENO,
etc.
"""
if span_id == const.NO_INTEGER:
Expand Down Expand Up @@ -914,10 +914,40 @@ def GetVar(self, name, lookup_mode=scope_e.Dynamic):
# Temp stacks are ignored
return runtime.StrArray(strs) # TODO: Reuse this object too?

# This isn't the call source, it's the source of the function DEFINITION
# (or the sourced # file itself).
if name == 'BASH_SOURCE':
strs = []
for func_name, source_name, _, _, _ in reversed(self.debug_stack):
if source_name:
strs.append(source_name)
return runtime.StrArray(strs) # TODO: Reuse this object too?

# This is how bash source SHOULD be defined, but it's not!
if name == 'CALL_SOURCE':
strs = []
for func_name, source_name, call_spid, _, _ in reversed(self.debug_stack):
if call_spid == const.NO_INTEGER: # should only happen for the first entry
continue
span = self.arena.GetLineSpan(call_spid)
path, _ = self.arena.GetDebugInfo(span.line_id)
strs.append(path)
return runtime.StrArray(strs) # TODO: Reuse this object too?

if name == 'BASH_LINENO':
strs = []
for func_name, source_name, call_spid, _, _ in reversed(self.debug_stack):
if call_spid == const.NO_INTEGER: # should only happen for the first entry
continue
span = self.arena.GetLineSpan(call_spid)
_, line_num = self.arena.GetDebugInfo(span.line_id)
strs.append(str(line_num))
return runtime.StrArray(strs) # TODO: Reuse this object too?

if name == 'LINENO':
return self.line_num

# Instead of BASH_SOURCE. Using Oil _ convnetion.
# This is OSH-specific. Get rid of it in favor of ${BASH_SOURCE[0]} ?
if name == 'SOURCE_NAME':
return self.source_name

Expand Down
24 changes: 24 additions & 0 deletions demo/completion.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
#
# Completion scripts
#
# Usage:
# source demo/completion.sh <function name>

argv() {
spec/bin/argv.py "$@"
}

complete_foo() {
argv "$@"

# This value is used in main bash_completion script.

argv source "${BASH_SOURCE[@]}"
argv 'source[0]' "${BASH_SOURCE[0]}"

COMPREPLY=(one two three)
}

complete -F complete_foo foo

1 change: 0 additions & 1 deletion spec/builtin-getopts.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ echo a=$FLAG_a b=$FLAG_b c=$FLAG_c d=$FLAG_d e=$FLAG_e


#### Getopts parses the function's arguments
# NOTE: GLOBALS are set, not locals! Bad interface.
FLAG_h=0
FLAG_c=''
myfunc() {
Expand Down
20 changes: 17 additions & 3 deletions spec/introspect.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
# ${BASH_SOURCE[0]} does line up with $LINENO!
#
# Geez.
#
# In other words, BASH_SOURCE is about the DEFINITION. While FUNCNAME and
# BASH_LINENO are about the CALL.


#### ${FUNCNAME[@]} array
Expand Down Expand Up @@ -54,6 +57,15 @@ argv.py "${FUNCNAME[@]}"
[]
## END

#### ${BASH_SOURCE[0]} is the current file
argv.py "${BASH_SOURCE[@]}"
source spec/testdata/bash-source-simple.sh
# f # hm calling this function doesn't quite work?
## STDOUT:
[]
['spec/testdata/bash-source-simple.sh']
## END

#### ${BASH_SOURCE[@]} is a stack of source files for function calls
$SH spec/testdata/bash-source.sh
## STDOUT:
Expand All @@ -78,11 +90,13 @@ f() {
g # line 6
argv.py 'end F' "${BASH_LINENO[@]}"
}
argv.py ${BASH_LINENO[@]}
f # line 9
## STDOUT:
['begin F', '9']
['G', '6', '9']
['end F', '9']
[]
['begin F', '10']
['G', '6', '10']
['end F', '10']
## END

#### $LINENO is the current line, not line of function call
Expand Down
1 change: 1 addition & 0 deletions spec/osh-only.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ echo status=$?
## STDOUT:
x = (Str s:42)
status=0
'nonexistent' is not defined
status=1
## END

Expand Down
2 changes: 1 addition & 1 deletion test/spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ special-vars() {

# dash/mksh don't implement this.
introspect() {
sh-spec spec/introspect.test.sh --osh-failures-allowed 2 \
sh-spec spec/introspect.test.sh --osh-failures-allowed 1 \
$BASH $OSH_LIST "$@"
}

Expand Down

0 comments on commit 42b4f96

Please sign in to comment.