Skip to content

Commit

Permalink
[builtins] Implement declare/readonly/export -p (#671)
Browse files Browse the repository at this point in the history
Use an imperative form to support arrays with unset elements.

Addresses issue #647
  • Loading branch information
akinomyoga authored Mar 21, 2020
1 parent 3e4ef40 commit 091db69
Show file tree
Hide file tree
Showing 4 changed files with 557 additions and 19 deletions.
32 changes: 29 additions & 3 deletions core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,10 +1387,10 @@ def GetVar(self, name, lookup_mode=scope_e.Dynamic):

return value.Undef()

def GetCell(self, name):
# type: (str) -> cell
def GetCell(self, name, lookup_mode=scope_e.Dynamic):
# type: (str, scope_t) -> cell
"""For the 'repr' builtin."""
cell, _ = self._ResolveNameOnly(name, scope_e.Dynamic)
cell, _ = self._ResolveNameOnly(name, lookup_mode)
return cell

def Unset(self, lval, lookup_mode):
Expand Down Expand Up @@ -1497,6 +1497,32 @@ def GetAllVars(self):
result[name] = str_val.s
return result

def GetAllCells(self, lookup_mode=scope_e.Dynamic):
# type: (scope_t) -> Dict[str, cell]
"""Get all variables and their values, for 'set' builtin. """
result = {} # type: Dict[str, str]

if lookup_mode == scope_e.Dynamic:
scopes = self.var_stack
elif lookup_mode == scope_e.LocalOnly:
scopes = self.var_stack[-1:]
elif lookup_mode == scope_e.GlobalOnly:
scopes = self.var_stack[0:1]
elif lookup_mode == scope_e.LocalOrGlobal:
scopes = self.var_stack[0:1]
if len(self.var_stack) > 1:
scopes.append(self.var_stack[-1])
else:
raise AssertionError()

for scope in scopes:
for name, cell in iteritems(scope):
result[name] = cell
return result

def IsGlobalScope(self):
# type: () -> bool
return len(self.var_stack) == 1

def SetLocalString(mem, name, s):
# type: (Mem, str, str) -> None
Expand Down
138 changes: 124 additions & 14 deletions osh/builtin_assign.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from _devbuild.gen.option_asdl import builtin_i
from _devbuild.gen.runtime_asdl import (
value, value_e, value_t, value__MaybeStrArray,
value, value_e, value_t, value__Str, value__MaybeStrArray, value__AssocArray,
lvalue, scope_e,
cmd_value__Argv, cmd_value__Assign,
)
Expand All @@ -15,6 +15,7 @@
from frontend import match
from core import state
from mycpp import mylib
from osh import string_ops

from typing import cast, Dict, Tuple, Any, TYPE_CHECKING
if TYPE_CHECKING:
Expand All @@ -25,10 +26,122 @@
from frontend import arg_def


def _PrintVariables(mem, cmd_val, arg, print_flags, readonly=False, exported=False):
# type: (Mem, value_t, Any, bool, bool, bool) -> int
flag_g = getattr(arg, 'g', None)
flag_n = getattr(arg, 'n', None)
flag_r = getattr(arg, 'r', None)
flag_x = getattr(arg, 'x', None)
flag_a = getattr(arg, 'a', None)
flag_A = getattr(arg, 'A', None)

lookup_mode = scope_e.Dynamic
if cmd_val.builtin_id == builtin_i.local:
if flag_g and not mem.IsGlobalScope():
return 1
lookup_mode = scope_e.LocalOnly
elif flag_g:
lookup_mode = scope_e.GlobalOnly

if len(cmd_val.pairs) == 0:
print_all = True
cells = mem.GetAllCells(lookup_mode)
names = sorted(cells)
else:
print_all = False
names = []
cells = {}
for pair in cmd_val.pairs:
name = pair.lval.name
if pair.rval and pair.rval.tag_() == value_e.Str:
name += "=" + cast(value__Str, pair.rval).s
names.append(name)
cells[name] = None
else:
names.append(name)
cells[name] = mem.GetCell(name, lookup_mode)

count = 0
for name in names:
cell = cells[name]
if cell is None: continue
val = cell.val

if val.tag_() == value_e.Undef: continue
if readonly and not cell.readonly: continue
if exported and not cell.exported: continue
if flag_n == '-' and not cell.nameref: continue
if flag_n == '+' and cell.nameref: continue
if flag_r == '-' and not cell.readonly: continue
if flag_r == '+' and cell.readonly: continue
if flag_x == '-' and not cell.exported: continue
if flag_x == '+' and cell.exported: continue
if flag_a and val.tag_() != value_e.MaybeStrArray: continue
if flag_A and val.tag_() != value_e.AssocArray: continue

decl = []
if print_flags:
flags = []
if cell.nameref: flags.append('n')
if cell.readonly: flags.append('r')
if cell.exported: flags.append('x')
if val.tag_() == value_e.MaybeStrArray:
flags.append('a')
elif val.tag_() == value_e.AssocArray:
flags.append('A')
if len(flags) == 0: flags.append('-')

decl.extend(["declare -", ''.join(flags), " ", name])
else:
decl.append(name)

if val.tag_() == value_e.Str:
str_val = cast(value__Str, val)
decl.extend(["=", string_ops.ShellQuote(str_val.s)])
elif val.tag_() == value_e.MaybeStrArray:
array_val = cast(value__MaybeStrArray, val)
if None in array_val.strs:
# Note: Arrays with unset elements are printed in the form:
# declare -p arr=(); arr[3]='' arr[4]='foo' ...
decl.append("=()")
first = True
for i, element in enumerate(array_val.strs):
if element is not None:
if first:
decl.append(";")
first = False
decl.extend([" ", name, "[", str(i), "]=", string_ops.ShellQuote(element)])
else:
body = []
for element in array_val.strs:
if len(body) > 0: body.append(" ")
body.append(string_ops.ShellQuote(element or ''))
decl.extend(["=(", ''.join(body), ")"])
elif val.tag_() == value_e.AssocArray:
assoc_val = cast(value__AssocArray, val)
body = []
for key in sorted(assoc_val.d):
if len(body) > 0: body.append(" ")
key_quoted = string_ops.ShellQuote(key)
value_quoted = string_ops.ShellQuote(assoc_val.d[key] or '')
body.extend(["[", key_quoted, "]=", value_quoted])
if len(body) > 0:
decl.extend(["=(", ''.join(body), ")"])

print(''.join(decl))
count += 1

if print_all or count == len(names):
return 0
else:
return 1


if mylib.PYTHON:
EXPORT_SPEC = arg_def.Register('export')
EXPORT_SPEC.ShortFlag('-n')
EXPORT_SPEC.ShortFlag('-f') # stubbed
EXPORT_SPEC.ShortFlag('-p')
# Instead of Reader? Or just make everything take a reader/
# They should check for extra args?
#spec.AcceptsCmdVal()
Expand Down Expand Up @@ -60,6 +173,9 @@ def Run(self, cmd_val):
raise args.UsageError(
"doesn't accept -f because it's dangerous. (The code can usually be restructured with 'source')")

if arg.p or len(cmd_val.pairs) == 0:
return _PrintVariables(self.mem, cmd_val, arg, True, exported=True)

positional = cmd_val.argv[arg_index:]
if arg.n:
for pair in cmd_val.pairs:
Expand Down Expand Up @@ -112,6 +228,7 @@ def _ReconcileTypes(rval, arg, span_id):
# TODO: Check the consistency of -a and -A against values, here and below.
READONLY_SPEC.ShortFlag('-a')
READONLY_SPEC.ShortFlag('-A')
READONLY_SPEC.ShortFlag('-p')


class Readonly(object):
Expand All @@ -126,6 +243,9 @@ def Run(self, cmd_val):
arg_r.Next()
arg, arg_index = READONLY_SPEC.Parse(arg_r)

if arg.p or len(cmd_val.pairs) == 0:
return _PrintVariables(self.mem, cmd_val, arg, True, readonly=True)

for pair in cmd_val.pairs:
if pair.rval is None:
if arg.a:
Expand Down Expand Up @@ -206,19 +326,9 @@ def Run(self, cmd_val):
return status

if arg.p: # Lookup and print variables.
names = [pair.lval.name for pair in cmd_val.pairs]
if names:
for name in names:
val = self.mem.GetVar(name)
if val.tag != value_e.Undef:
# TODO: Print flags.

print(name)
else:
status = 1
else:
raise args.UsageError('declare/typeset -p without args')
return status
return _PrintVariables(self.mem, cmd_val, arg, True)
elif len(cmd_val.pairs) == 0:
return _PrintVariables(self.mem, cmd_val, arg, False)

#
# Set variables
Expand Down
2 changes: 1 addition & 1 deletion osh/string_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def ShellQuote(s):
It doesn't necessarily match bash byte-for-byte. IIRC bash isn't consistent
with it anyway.
Used for 'printf %q', ${x@Q}, and 'set'.
Used for 'printf %q', ${x@Q}, 'set', and `declare -p`.
"""
# Could be made slightly nicer by e.g. returning unmodified when
# there's nothing that needs to be quoted. Bash's `printf %q`
Expand Down
Loading

0 comments on commit 091db69

Please sign in to comment.