Skip to content

Commit

Permalink
[core/dev] set -x prints something useful for assignments.
Browse files Browse the repository at this point in the history
Addresses issue #882.
  • Loading branch information
Andy Chu committed Jan 12, 2021
1 parent 451ed31 commit c58b0ae
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 32 deletions.
100 changes: 87 additions & 13 deletions core/dev.py
Expand Up @@ -3,7 +3,12 @@
"""
from __future__ import print_function

from _devbuild.gen.runtime_asdl import value_e, value__Str
from _devbuild.gen.runtime_asdl import (
value_e, value__Str, value__MaybeStrArray, value__AssocArray,
lvalue_e, lvalue__Named, lvalue__Indexed, lvalue__Keyed,
cmd_value__Assign
)
from _devbuild.gen.option_asdl import builtin_i
from _devbuild.gen.syntax_asdl import assign_op_e

from asdl import runtime
Expand All @@ -14,6 +19,7 @@
from osh import word_
from pylib import os_path
from mycpp import mylib
from mycpp.mylib import switch, tagswitch, iteritems

import posix_ as posix

Expand Down Expand Up @@ -184,7 +190,7 @@ def __init__(self,
self.mutable_opts = mutable_opts
self.mem = mem
self.word_ev = word_ev
self.f = f # can be the --debug-file as well
self.f = f # can be stderr, the --debug-file, etc.

# PS4 value -> compound_word. PS4 is scoped.
self.parse_cache = {} # type: Dict[str, compound_word]
Expand Down Expand Up @@ -234,31 +240,99 @@ def _EvalPS4(self):
self.mutable_opts.set_xtrace(True)
return first_char, prefix.s

def _TraceBegin(self):
# type: () -> bool
if not self.exec_opts.xtrace():
return False

first_char, prefix = self._EvalPS4()
self.f.write(first_char)
self.f.write(prefix)
return True

def _Value(self, val):
# type: (value_t) -> None

# NOTE: This is a bit like _PrintVariables for declare -p
result = '?'
UP_val = val
with tagswitch(val) as case:
if case(value_e.Str):
val = cast(value__Str, UP_val)
result = qsn.maybe_shell_encode(val.s)

elif case(value_e.MaybeStrArray):
val = cast(value__MaybeStrArray, UP_val)
parts = ['(']
for s in val.strs:
parts.append(qsn.maybe_shell_encode(s))
parts.append(')')
result = ' '.join(parts)

elif case(value_e.AssocArray):
val = cast(value__AssocArray, UP_val)
parts = ['(']
for k, v in iteritems(val.d):
parts.append('[%s]=%s' % (
qsn.maybe_shell_encode(k), qsn.maybe_shell_encode(v)))
parts.append(')')
result = ' '.join(parts)

self.f.write(result)

def OnSimpleCommand(self, argv):
# type: (List[str]) -> None
# NOTE: I think tracing should be on by default? For post-mortem viewing.
if not self.exec_opts.xtrace():
if not self._TraceBegin():
return

first_char, prefix = self._EvalPS4()
tmp = [qsn.maybe_shell_encode(a) for a in argv]
cmd = ' '.join(tmp)
self.f.log('%s%s%s', first_char, prefix, cmd)
self.f.write(' '.join(tmp))
self.f.write('\n')

def OnAssignBuiltin(self, cmd_val):
# type: (cmd_value__Assign) -> None
if not self._TraceBegin():
return

for arg in cmd_val.argv:
self.f.write(arg)
self.f.write(' ')

for pair in cmd_val.pairs:
self.f.write(pair.var_name)
self.f.write('=')
if pair.rval:
self._Value(pair.rval)
self.f.write(' ')

self.f.write('\n')

def OnShAssignment(self, lval, op, val, flags, which_scopes):
# type: (lvalue_t, assign_op_t, value_t, int, scope_t) -> None
# NOTE: I think tracing should be on by default? For post-mortem viewing.
if not self.exec_opts.xtrace():
if not self._TraceBegin():
return

first_char, prefix = self._EvalPS4()
left = '?'
UP_lval = lval
with tagswitch(lval) as case:
if case(lvalue_e.Named):
lval = cast(lvalue__Named, UP_lval)
left = lval.name
elif case(lvalue_e.Indexed):
lval = cast(lvalue__Indexed, UP_lval)
left = '%s[%d]' % (lval.name, lval.index)
elif case(lvalue_e.Keyed):
lval = cast(lvalue__Keyed, UP_lval)
left = '%s[%s]' % (lval.name, qsn.maybe_shell_encode(lval.key))
self.f.write(left)

# Only two possibilities here
op_str = '+=' if op == assign_op_e.PlusEqual else '='
self.f.write(op_str)

self._Value(val)

# TODO: Need a way to print arbitrary 'lval' and 'val' here
if mylib.PYTHON:
self.f.log('%s%s%s %s %s', first_char, prefix, lval, op_str, val)
self.f.write('\n')

def Event(self):
# type: () -> None
Expand Down
4 changes: 1 addition & 3 deletions core/util.py
Expand Up @@ -80,7 +80,7 @@ def log(self, msg, *args):
msg = msg % args
self.f.write(msg)
self.f.write('\n')
self.f.flush() # need to see it interacitvely
self.f.flush() # need to see it interactively

# These two methods are for node.PrettyPrint()
def write(self, s):
Expand All @@ -90,5 +90,3 @@ def write(self, s):
def isatty(self):
# type: () -> bool
return self.f.isatty()


91 changes: 85 additions & 6 deletions demo/xtrace1.sh
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
#
# Usage:
# ./xtrace1.sh <function name>
# demo/xtrace1.sh <function name>

set -o nounset
set -o pipefail
set -o errexit
#set -o nounset
#set -o pipefail
#set -o errexit

# Problem:
# - There is no indentation for function calls
Expand Down Expand Up @@ -63,19 +63,98 @@ main() {
echo foo
}

my-ps4() {
my_ps4() {
for i in {1..3}; do
echo -n $i
done
}

# The problem with this is you don't want to fork the shell for every line!

call-func-in-ps4() {
call_func_in_ps4() {
set -x
PS4='[$(my-ps4)] '
echo one
echo two
}

# EXPANDED argv is displayed, NOT the raw input.
# - OK just do assignments?

# - bash shows the 'for x in 1 2 3' all on one line
# - dash doesn't show the 'for'
# - neither does zsh and mksh
# - zsh shows line numbers and the function name!

# - two statements on one line are broken up

# - bash doesn't show 'while'

# The $((i+1)) is evaluated. Hm.

# Hm we don't implement this, only works at top level
# set -v

loop() {
set -x

for x in 1 \
2 \
3; do
echo $x; echo =$(echo {x}-)
done

i=0
while test $i -lt 3; do
echo $x; echo ${x}-
i=$((i+1))
done
}

atoms1() {
set -x

foo=bar

# this messes up a lot of printing. OSH will use QSN.
x='one
two'

i=1

[[ -n $x ]]
echo "$x"

# $i gets expanded, not i
(( y = 42 + i + $i ))
}

atoms2() {
set -x

x='one
two'

declare -a a
a[1]="$x"

# This works
declare -A A
A["$x"]=1

a=(1 2 3)
A=([k]=v)

a=("$x" $x)
A=([k]="$x")

# Assignment builtins

declare -g -r d=0 foo=bar
typeset t=1
local lo=2
export e=3 f=foo
readonly r=4
}

"$@"
1 change: 0 additions & 1 deletion osh/builtin_assign.py
Expand Up @@ -217,7 +217,6 @@ def Run(self, cmd_val):
if arg.p or len(cmd_val.pairs) == 0:
return _PrintVariables(self.mem, cmd_val, attrs, True, builtin=_EXPORT)


if arg.n:
for pair in cmd_val.pairs:
if pair.rval is not None:
Expand Down
17 changes: 8 additions & 9 deletions osh/cmd_eval.py
Expand Up @@ -598,18 +598,19 @@ def _Dispatch(self, node, pipeline_st):
UP_cmd_val = cmd_val
if UP_cmd_val.tag_() == cmd_value_e.Argv:
cmd_val = cast(cmd_value__Argv, UP_cmd_val)
argv = cmd_val.argv
cmd_val.block = node.block # may be None

# This comes before evaluating env, in case there are problems evaluating
# it. We could trace the env separately? Also trace unevaluated code
# with set-o verbose?
self.tracer.OnSimpleCommand(cmd_val.argv)

else:
argv = ['TODO: trace string for assignment']
if node.block:
e_die("ShAssignment builtins don't accept blocks",
span_id=node.block.spids[0])

# This comes before evaluating env, in case there are problems evaluating
# it. We could trace the env separately? Also trace unevaluated code
# with set-o verbose?
self.tracer.OnSimpleCommand(argv)
cmd_val = cast(cmd_value__Assign, UP_cmd_val)
self.tracer.OnAssignBuiltin(cmd_val)

# NOTE: RunSimpleCommand never returns when do_fork=False!
if len(node.more_env): # I think this guard is necessary?
Expand Down Expand Up @@ -753,8 +754,6 @@ def _Dispatch(self, node, pipeline_st):
elif case2(Id.KW_SetGlobal):
which_scopes = scope_e.GlobalOnly
elif case2(Id.KW_SetRef):
# TODO: setref upvalue = 'returned'
# Require the cell.nameref flag on it.
which_scopes = scope_e.Dynamic
else:
raise AssertionError(node.keyword.id)
Expand Down

0 comments on commit c58b0ae

Please sign in to comment.