Permalink
Browse files

Initialize shell options from the SHELLOPTS environment variable.

Also update it whenever options are changed.

You can do 'export SHELLOPTS' to inherit shell options across
invocations.

Use case: we want child shells (not just subshells) to inherit 'xtrace'.
  • Loading branch information...
Andy Chu
Andy Chu committed Dec 27, 2017
1 parent e193478 commit 934deba65e747ebdfff18c6ac3aed302344c805a
Showing with 106 additions and 15 deletions.
  1. +2 −1 bin/oil.py
  2. +0 −1 core/builtin.py
  3. +2 −2 core/cmd_exec_test.py
  4. +2 −1 core/completion.py
  5. +3 −2 core/completion_test.py
  6. +77 −6 core/state.py
  7. +1 −1 osh/arith_parse_test.py
  8. +18 −0 spec/sh-options.test.sh
  9. +1 −1 test/spec.sh
View
@@ -248,7 +248,8 @@ def OshMain(argv, login_shell):
else:
# TODO: NullLookup?
comp_lookup = None
exec_opts = state.ExecOpts()
exec_opts = state.ExecOpts(mem)
builtin.SetExecOpts(exec_opts, opts.opt_changes)
# TODO: How to get a handle to initialized builtins here?
View
@@ -815,7 +815,6 @@ def Shopt(argv, exec_opts):
if b is None:
raise NotImplementedError # Display options
# TODO: exec_opts.SetShoptOption()
for opt_name in argv[i:]:
if arg.o:
exec_opts.SetOption(opt_name, b)
View
@@ -44,7 +44,7 @@ def InitExecutor():
builtins = builtin.BUILTIN_DEF
funcs = {}
comp_funcs = {}
exec_opts = state.ExecOpts()
exec_opts = state.ExecOpts(mem)
pool = alloc.Pool()
arena = pool.NewArena()
return cmd_exec.Executor(mem, status_lines, funcs, completion, comp_funcs,
@@ -56,7 +56,7 @@ def InitEvaluator():
state.SetLocalString(mem, 'x', 'xxx')
state.SetLocalString(mem, 'y', 'yyy')
exec_opts = state.ExecOpts()
exec_opts = state.ExecOpts(mem)
# Don't need side effects for most things
return word_eval.CompletionWordEvaluator(mem, exec_opts)
View
@@ -788,7 +788,8 @@ def Init(pool, builtins, mem, funcs, comp_lookup, status_out, ev):
from core import state
status_lines = ui.MakeStatusLines()
exec_opts = state.ExecOpts()
mem = state.Mem('', [], {}, None)
exec_opts = state.ExecOpts(mem)
status_out = StatusOutput(status_lines, exec_opts)
builtins = builtin.BUILTIN_DEF
View
@@ -33,7 +33,8 @@
C1 = completion.ChainedCompleter([A1])
status_lines = [ui.TestStatusLine()] * 10 # A bunch of dummies
exec_opts = state.ExecOpts()
mem = state.Mem('', [], {}, None)
exec_opts = state.ExecOpts(mem)
STATUS = completion.StatusOutput(status_lines, exec_opts)
@@ -144,7 +145,7 @@ def testRootCompleter(self):
def _MakeTestEvaluator():
mem = state.Mem('', [], {}, None)
exec_opts = state.ExecOpts()
exec_opts = state.ExecOpts(mem)
ev = word_eval.CompletionWordEvaluator(mem, exec_opts)
return ev
View
@@ -83,7 +83,13 @@ def Set(self, b):
class ExecOpts(object):
def __init__(self):
def __init__(self, mem):
"""
Args:
mem: state.Mem, for SHELLOPTS
"""
self.mem = mem
# set -o / set +o
self.errexit = _ErrExit() # -e
self.nounset = False # -u
@@ -95,6 +101,10 @@ def __init__(self):
self.debug_completion = False
self.strict_control_flow = False
shellopts = self.mem.GetVar('SHELLOPTS')
assert shellopts.tag == value_e.Str, shellopts
self._InitFromEnv(shellopts.s)
# shopt -s / -u
self.nullglob = False
self.failglob = False
@@ -108,6 +118,13 @@ def __init__(self):
# TODO: strict_bool. Some of this is covered by arithmetic, e.g. -eq.
def _InitFromEnv(self, shellopts):
# e.g. errexit:nounset:pipefail
lookup = set(shellopts.split(':'))
for _, name in SET_OPTIONS:
if name in lookup:
self._SetOption(name, True)
def ErrExit(self):
return self.errexit.errexit
@@ -129,11 +146,8 @@ def GetDollarHyphen(self):
# - B for brace expansion
return ''.join(chars)
# TODO: Share with the other spec
SET_OPTIONS = ('errexit',)
def SetOption(self, opt_name, b):
""" For set -o, set +o, or shopt -s/-u -o. """
def _SetOption(self, opt_name, b):
"""Private version for synchronizing from SHELLOPTS."""
assert '_' not in opt_name
if opt_name not in _SET_OPTION_NAMES:
raise args.UsageError('Invalid option %r' % opt_name)
@@ -144,6 +158,34 @@ def SetOption(self, opt_name, b):
opt_name = opt_name.replace('-', '_')
setattr(self, opt_name, b)
def SetOption(self, opt_name, b):
""" For set -o, set +o, or shopt -s/-u -o. """
self._SetOption(opt_name, b)
val = self.mem.GetVar('SHELLOPTS')
assert val.tag == value_e.Str
shellopts = val.s
# Now check if SHELLOPTS needs to be updated. It may be exported.
#
# NOTE: It might be better to skip rewriting SEHLLOPTS in the common case
# where it is not used. We could do it lazily upon GET.
# Also, it would be slightly more efficient to update SHELLOPTS if
# settings were batched, Examples:
# - set -eu
# - shopt -s foo bar
if b:
if opt_name not in shellopts:
new_val = runtime.Str('%s:%s' % (shellopts, opt_name))
self.mem.InternalSetGlobal('SHELLOPTS', new_val)
else:
if opt_name in shellopts:
names = shellopts.split(':')
names = [n for n in names if n != opt_name]
new_val = runtime.Str(':'.join(names))
self.mem.InternalSetGlobal('SHELLOPTS', new_val)
SHOPT_OPTIONS = ('nullglob', 'failglob')
def SetShoptOption(self, opt_name, b):
@@ -288,6 +330,16 @@ def _InitEnviron(self, environ):
self.SetVar(ast.LhsName(n), runtime.Str(v),
(var_flags_e.Exported,), scope_e.GlobalOnly)
# If it's not in the environment, initialize it. This makes it easier to
# update later in ExecOpts.
v = self.GetVar('SHELLOPTS')
if v.tag == value_e.Undef:
SetGlobalString(self, 'SHELLOPTS', '')
# Now make it readonly
self.SetVar(
ast.LhsName('SHELLOPTS'), None, (var_flags_e.ReadOnly,),
scope_e.GlobalOnly)
#
# Stack
#
@@ -532,6 +584,20 @@ def SetVar(self, lval, value, new_flags, lookup_mode):
else:
raise AssertionError
def InternalSetGlobal(self, name, new_val):
"""For setting read-only globals internally.
Args:
name: string (not Lhs)
new_val: value
The variable must already exist.
Use case: SHELLOPTS.
"""
cell = self.var_stack[0][name]
cell.val = new_val
# NOTE: Have a default for convenience
def GetVar(self, name, lookup_mode=scope_e.Dynamic):
assert isinstance(name, str), name
@@ -586,6 +652,11 @@ def ClearFlag(self, name, flag, lookup_mode):
def GetExported(self):
"""Get all the variables that are marked exported."""
# TODO: This is run on every SimpleCommand. Should we have a dirty flag?
# We have to notice these things:
# - If an exported variable is changed.
# - If the set of exported variables changes.
exported = {}
# Search from globals up. Names higher on the stack will overwrite names
# lower on the stack.
View
@@ -37,7 +37,7 @@ def ParseAndEval(code_str):
print('node:', anode)
mem = state.Mem('', [])
exec_opts = state.ExecOpts()
exec_opts = state.ExecOpts(mem)
ev = word_eval.CompletionWordEvaluator(mem, exec_opts)
arith_ev = expr_eval.ArithEvaluator(mem, ev, exec_opts)
View
@@ -206,3 +206,21 @@ echo foo > $TMP/no-clobber
echo $?
# stdout-json: "0\n1\n"
# OK dash stdout-json: "0\n2\n"
### SHELLOPTS is updated when options are changed
echo $SHELLOPTS | grep -q xtrace
echo $?
set -x
echo $SHELLOPTS | grep -q xtrace
echo $?
set +x
echo $SHELLOPTS | grep -q xtrace
echo $?
# stdout-json: "1\n0\n1\n"
# N-I dash/mksh stdout-json: "1\n1\n1\n"
### SHELLOPTS is readonly
SHELLOPTS=x
echo status=$?
# stdout: status=1
# N-I dash/mksh stdout: status=0
View
@@ -412,7 +412,7 @@ var-sub-quote() {
}
sh-options() {
sh-spec spec/sh-options.test.sh --osh-failures-allowed 3 \
sh-spec spec/sh-options.test.sh --osh-failures-allowed 4 \
${REF_SHELLS[@]} $OSH "$@"
}

0 comments on commit 934deba

Please sign in to comment.