| 1 | #!/usr/bin/env python2
 | 
| 2 | from __future__ import print_function
 | 
| 3 | 
 | 
| 4 | from typing import List, Dict, Optional, Any
 | 
| 5 | 
 | 
| 6 | 
 | 
| 7 | class Option(object):
 | 
| 8 | 
 | 
| 9 |     def __init__(self,
 | 
| 10 |                  index,
 | 
| 11 |                  name,
 | 
| 12 |                  short_flag=None,
 | 
| 13 |                  builtin='shopt',
 | 
| 14 |                  default=False,
 | 
| 15 |                  implemented=True,
 | 
| 16 |                  groups=None):
 | 
| 17 |         # type: (int, str, str, Optional[str], bool, bool, List[str]) -> None
 | 
| 18 |         self.index = index
 | 
| 19 |         self.name = name  # e.g. 'errexit'
 | 
| 20 |         self.short_flag = short_flag  # 'e' for -e
 | 
| 21 | 
 | 
| 22 |         if short_flag:
 | 
| 23 |             self.builtin = 'set'
 | 
| 24 |         else:
 | 
| 25 |             # The 'interactive' option is the only one where builtin is None.  It has
 | 
| 26 |             # a cell but you can't change it.  Only the shell can.
 | 
| 27 |             self.builtin = builtin
 | 
| 28 | 
 | 
| 29 |         self.default = default  # default value is True in some cases
 | 
| 30 |         self.implemented = implemented
 | 
| 31 |         self.groups = groups or []  # list of groups
 | 
| 32 | 
 | 
| 33 |         # for optview
 | 
| 34 |         self.is_parse = name.startswith('parse_') or name == 'expand_aliases'
 | 
| 35 |         # interactive() is an accessor
 | 
| 36 |         self.is_exec = implemented and not self.is_parse
 | 
| 37 | 
 | 
| 38 | 
 | 
| 39 | class _OptionDef(object):
 | 
| 40 |     """Description of all shell options.
 | 
| 41 | 
 | 
| 42 |     Similar to id_kind_def.IdSpec
 | 
| 43 |     """
 | 
| 44 | 
 | 
| 45 |     def __init__(self):
 | 
| 46 |         # type: () -> None
 | 
| 47 |         self.opts = []  # type: List[Option]
 | 
| 48 |         self.index = 1  # start with 1
 | 
| 49 |         self.array_size = -1
 | 
| 50 | 
 | 
| 51 |     def Add(self, *args, **kwargs):
 | 
| 52 |         # type: (Any, Any) -> None
 | 
| 53 |         self.opts.append(Option(self.index, *args, **kwargs))
 | 
| 54 |         self.index += 1
 | 
| 55 | 
 | 
| 56 |     def DoneWithImplementedOptions(self):
 | 
| 57 |         # type: () -> None
 | 
| 58 |         self.array_size = self.index
 | 
| 59 | 
 | 
| 60 | 
 | 
| 61 | # Used by builtin
 | 
| 62 | _OTHER_SET_OPTIONS = [
 | 
| 63 |     # NOTE: set -i and +i is explicitly disallowed.  Only osh -i or +i is valid
 | 
| 64 |     # https://unix.stackexchange.com/questions/339506/can-an-interactive-shell-become-non-interactive-or-vice-versa
 | 
| 65 |     ('n', 'noexec'),
 | 
| 66 |     ('x', 'xtrace'),
 | 
| 67 |     ('v', 'verbose'),  # like xtrace, but prints unevaluated commands
 | 
| 68 |     ('f', 'noglob'),
 | 
| 69 |     ('C', 'noclobber'),
 | 
| 70 | 
 | 
| 71 |     # A no-op for modernish.
 | 
| 72 |     (None, 'posix'),
 | 
| 73 |     (None, 'vi'),
 | 
| 74 |     (None, 'emacs'),
 | 
| 75 | ]
 | 
| 76 | 
 | 
| 77 | # These are RUNTIME strict options.  We also have parse time ones like
 | 
| 78 | # parse_backslash.
 | 
| 79 | _STRICT_OPTS = [
 | 
| 80 |     'strict_argv',  # empty argv not allowed
 | 
| 81 |     'strict_arith',  # string to integer conversions, e.g. x=foo; echo $(( x ))
 | 
| 82 | 
 | 
| 83 |     # No implicit conversions between string and array.
 | 
| 84 |     # - foo="$@" not allowed because it decays.  Should be foo=( "$@" ).
 | 
| 85 |     # - ${a} not ${a[0]} (not implemented)
 | 
| 86 |     # sane-array?  compare arrays like [[ "$@" == "${a[@]}" ]], which is
 | 
| 87 |     #              incompatible because bash coerces
 | 
| 88 |     # default:    do not allow
 | 
| 89 |     'strict_array',
 | 
| 90 |     'strict_control_flow',  # break/continue at top level is fatal
 | 
| 91 |     # 'return $empty' and return "" are NOT accepted
 | 
| 92 |     'strict_errexit',  # errexit can't be disabled in compound commands
 | 
| 93 |     'strict_nameref',  # trap invalid variable names
 | 
| 94 |     'strict_word_eval',  # negative slices, unicode
 | 
| 95 |     'strict_tilde',  # ~nonexistent is an error (like zsh)
 | 
| 96 | 
 | 
| 97 |     # Not implemented
 | 
| 98 |     'strict_glob',  # glob_.py GlobParser has warnings
 | 
| 99 | ]
 | 
| 100 | 
 | 
| 101 | # These will break some programs, but the fix should be simple.
 | 
| 102 | 
 | 
| 103 | # command_sub_errexit makes 'local foo=$(false)' and echo $(false) fail.
 | 
| 104 | # By default, we have mimic bash's undesirable behavior of ignoring
 | 
| 105 | # these failures, since ash copied it, and Alpine's abuild relies on it.
 | 
| 106 | #
 | 
| 107 | # Note that inherit_errexit is a strict option.
 | 
| 108 | 
 | 
| 109 | _UPGRADE_RUNTIME_OPTS = [
 | 
| 110 |     ('simple_word_eval', False),  # No splitting; arity isn't data-dependent
 | 
| 111 |     # Don't reparse program data as globs
 | 
| 112 |     ('dashglob', True),  # do globs return files starting with - ?
 | 
| 113 | 
 | 
| 114 |     # TODO: Should these be in strict mode?
 | 
| 115 |     # The logic was that strict_errexit improves your bash programs, but these
 | 
| 116 |     # would lead you to remove error handling.  But the same could be said for
 | 
| 117 |     # strict_array?
 | 
| 118 |     ('command_sub_errexit', False),  # check after command sub
 | 
| 119 |     ('process_sub_fail', False),  # like pipefail, but for <(sort foo.txt)
 | 
| 120 |     ('xtrace_rich', False),  # Hierarchical trace with PIDs
 | 
| 121 |     ('xtrace_details', True),  # Legacy set -x stuff
 | 
| 122 | 
 | 
| 123 |     # Whether status 141 in pipelines is turned into 0
 | 
| 124 |     ('sigpipe_status_ok', False),
 | 
| 125 | 
 | 
| 126 |     # This applies to shell functions too
 | 
| 127 |     # It's also turned on in interactive mode
 | 
| 128 |     ('redefine_proc_func', True),
 | 
| 129 | ]
 | 
| 130 | 
 | 
| 131 | # TODO: Add strict_arg_parse?  For example, 'trap 1 2 3' shouldn't be
 | 
| 132 | # valid, because it has an extra argument.  Builtins are inconsistent about
 | 
| 133 | # checking this.
 | 
| 134 | 
 | 
| 135 | _YSH_RUNTIME_OPTS = [
 | 
| 136 |     ('simple_echo', False),  # echo takes 0 or 1 arguments
 | 
| 137 |     ('simple_eval_builtin', False),  # eval takes exactly 1 argument
 | 
| 138 | 
 | 
| 139 |     # only file tests (no strings), remove [, status 2
 | 
| 140 |     ('simple_test_builtin', False),
 | 
| 141 | 
 | 
| 142 |     # TODO: simple_trap
 | 
| 143 | 
 | 
| 144 |     # Turn aliases off so we can statically parse.  bash has it off
 | 
| 145 |     # non-interactively, sothis shouldn't break much.
 | 
| 146 |     ('expand_aliases', True),
 | 
| 147 | ]
 | 
| 148 | 
 | 
| 149 | # Stuff that doesn't break too many programs.
 | 
| 150 | _UPGRADE_PARSE_OPTS = [
 | 
| 151 |     'parse_at',  # @array, @[expr]
 | 
| 152 |     'parse_proc',  # proc p { ... }
 | 
| 153 |     'parse_func',  # func f(x) { ... }
 | 
| 154 |     'parse_brace',  # cd /bin { ... }
 | 
| 155 |     'parse_bracket',  # assert [42 === x]
 | 
| 156 | 
 | 
| 157 |     # bare assignment 'x = 42' is allowed in Hay { } blocks, but disallowed
 | 
| 158 |     # everywhere else.  It's not a command 'x' with arg '='.
 | 
| 159 |     'parse_equals',
 | 
| 160 |     'parse_paren',  # if (x > 0) ...
 | 
| 161 |     'parse_ysh_string',  # r'' u'' b'' and multi-line versions
 | 
| 162 |     'parse_triple_quote',  # for ''' and """
 | 
| 163 | ]
 | 
| 164 | 
 | 
| 165 | # Extra stuff that breaks too many programs.
 | 
| 166 | _YSH_PARSE_OPTS = [
 | 
| 167 |     ('parse_at_all', False),  # @ starting any word, e.g. @[] @{} @@ @_ @-
 | 
| 168 | 
 | 
| 169 |     # Legacy syntax that is removed.  These options are distinct from strict_*
 | 
| 170 |     # because they don't help you avoid bugs in bash programs.  They just makes
 | 
| 171 |     # the language more consistent.
 | 
| 172 |     ('parse_backslash', True),
 | 
| 173 |     ('parse_backticks', True),
 | 
| 174 |     ('parse_dollar', True),
 | 
| 175 |     ('parse_ignored', True),
 | 
| 176 |     ('parse_sh_arith', True),  # disallow all shell arithmetic, $(( )) etc.
 | 
| 177 |     ('parse_dparen', True),  # disallow bash's ((
 | 
| 178 |     ('parse_dbracket', True),  # disallow bash's [[
 | 
| 179 |     ('parse_bare_word', True),  # 'case bare' and 'for x in bare'
 | 
| 180 | ]
 | 
| 181 | 
 | 
| 182 | # No-ops for bash compatibility
 | 
| 183 | _NO_OPS = [
 | 
| 184 |     'lastpipe',  # this feature is always on
 | 
| 185 | 
 | 
| 186 |     # Handled one by one
 | 
| 187 |     'progcomp',
 | 
| 188 |     'histappend',  # stubbed out for issue #218
 | 
| 189 |     'hostcomplete',  # complete words with '@' ?
 | 
| 190 |     'cmdhist',  # multi-line commands in history
 | 
| 191 | 
 | 
| 192 |     # Copied from https://www.gnu.org/software/bash/manual/bash.txt
 | 
| 193 |     # except 'compat*' because they were deemed too ugly
 | 
| 194 |     'assoc_expand_once',
 | 
| 195 |     'autocd',
 | 
| 196 |     'cdable_vars',
 | 
| 197 |     'cdspell',
 | 
| 198 |     'checkhash',
 | 
| 199 |     'checkjobs',
 | 
| 200 |     'checkwinsize',
 | 
| 201 |     'complete_fullquote',  # Set by default
 | 
| 202 |     # If set, Bash quotes all shell metacharacters in filenames and
 | 
| 203 |     # directory names when performing completion.  If not set, Bash
 | 
| 204 |     # removes metacharacters such as the dollar sign from the set of
 | 
| 205 |     # characters that will be quoted in completed filenames when
 | 
| 206 |     # these metacharacters appear in shell variable references in
 | 
| 207 |     # words to be completed.  This means that dollar signs in
 | 
| 208 |     # variable names that expand to directories will not be quoted;
 | 
| 209 |     # however, any dollar signs appearing in filenames will not be
 | 
| 210 |     # quoted, either.  This is active only when bash is using
 | 
| 211 |     # backslashes to quote completed filenames.  This variable is
 | 
| 212 |     # set by default, which is the default Bash behavior in versions
 | 
| 213 |     # through 4.2.
 | 
| 214 |     'direxpand',
 | 
| 215 |     'dirspell',
 | 
| 216 |     'dotglob',
 | 
| 217 |     'execfail',
 | 
| 218 |     'extdebug',  # for --debugger?
 | 
| 219 |     'extquote',
 | 
| 220 |     'force_fignore',
 | 
| 221 |     'globasciiranges',
 | 
| 222 |     'globstar',  # TODO:  implement **
 | 
| 223 |     'gnu_errfmt',
 | 
| 224 |     'histreedit',
 | 
| 225 |     'histverify',
 | 
| 226 |     'huponexit',
 | 
| 227 |     'interactive_comments',
 | 
| 228 |     'lithist',
 | 
| 229 |     'localvar_inherit',
 | 
| 230 |     'localvar_unset',
 | 
| 231 |     'login_shell',
 | 
| 232 |     'mailwarn',
 | 
| 233 |     'no_empty_cmd_completion',
 | 
| 234 |     'nocaseglob',
 | 
| 235 |     'progcomp_alias',
 | 
| 236 |     'promptvars',
 | 
| 237 |     'restricted_shell',
 | 
| 238 |     'shift_verbose',
 | 
| 239 |     'sourcepath',
 | 
| 240 |     'xpg_echo',
 | 
| 241 | ]
 | 
| 242 | 
 | 
| 243 | 
 | 
| 244 | def _Init(opt_def):
 | 
| 245 |     # type: (_OptionDef) -> None
 | 
| 246 | 
 | 
| 247 |     opt_def.Add('errexit',
 | 
| 248 |                 short_flag='e',
 | 
| 249 |                 builtin='set',
 | 
| 250 |                 groups=['ysh:upgrade', 'ysh:all'])
 | 
| 251 |     opt_def.Add('nounset',
 | 
| 252 |                 short_flag='u',
 | 
| 253 |                 builtin='set',
 | 
| 254 |                 groups=['ysh:upgrade', 'ysh:all'])
 | 
| 255 |     opt_def.Add('pipefail', builtin='set', groups=['ysh:upgrade', 'ysh:all'])
 | 
| 256 | 
 | 
| 257 |     opt_def.Add('inherit_errexit', groups=['ysh:upgrade', 'ysh:all'])
 | 
| 258 |     # Hm is this subsumed by simple_word_eval?
 | 
| 259 |     opt_def.Add('nullglob', groups=['ysh:upgrade', 'ysh:all'])
 | 
| 260 |     opt_def.Add('verbose_errexit', groups=['ysh:upgrade', 'ysh:all'])
 | 
| 261 | 
 | 
| 262 |     # set -o noclobber, etc.
 | 
| 263 |     for short_flag, name in _OTHER_SET_OPTIONS:
 | 
| 264 |         opt_def.Add(name, short_flag=short_flag, builtin='set')
 | 
| 265 | 
 | 
| 266 |     # The only one where builtin=None.  Only the shell can change it.
 | 
| 267 |     opt_def.Add('interactive', builtin=None)
 | 
| 268 | 
 | 
| 269 |     # bash --norc -c 'set -o' shows this is on by default
 | 
| 270 |     opt_def.Add('hashall', short_flag='h', builtin='set', default=True)
 | 
| 271 | 
 | 
| 272 |     #
 | 
| 273 |     # shopt
 | 
| 274 |     # (bash uses $BASHOPTS rather than $SHELLOPTS)
 | 
| 275 |     #
 | 
| 276 | 
 | 
| 277 |     # shopt options that aren't in any groups.
 | 
| 278 |     opt_def.Add('failglob')
 | 
| 279 |     opt_def.Add('extglob')
 | 
| 280 |     opt_def.Add('nocasematch')
 | 
| 281 | 
 | 
| 282 |     # recursive parsing and evaluation - for compatibility, ble.sh, etc.
 | 
| 283 |     opt_def.Add('eval_unsafe_arith')
 | 
| 284 | 
 | 
| 285 |     # For implementing strict_errexit
 | 
| 286 |     # TODO: could be _no_command_sub / _no_process_sub, if we had to discourage
 | 
| 287 |     # "default True" options
 | 
| 288 |     opt_def.Add('_allow_command_sub', default=True)
 | 
| 289 |     opt_def.Add('_allow_process_sub', default=True)
 | 
| 290 | 
 | 
| 291 |     # For implementing 'proc'
 | 
| 292 |     opt_def.Add('dynamic_scope', default=True)
 | 
| 293 | 
 | 
| 294 |     # On in interactive shell
 | 
| 295 |     opt_def.Add('redefine_module', default=False)
 | 
| 296 |     # Hm these aren't the same?
 | 
| 297 |     #opt_def.Add('redefine_proc_func', default=False),
 | 
| 298 | 
 | 
| 299 |     # For disabling strict_errexit while running traps.  Because we run in the
 | 
| 300 |     # main loop, the value can be "off".  Prefix with _ because it's undocumented
 | 
| 301 |     # and users shouldn't fiddle with it.  We need a stack so this is a
 | 
| 302 |     # convenient place.
 | 
| 303 |     opt_def.Add('_running_trap')
 | 
| 304 |     opt_def.Add('_running_hay')
 | 
| 305 | 
 | 
| 306 |     # For fixing lastpipe / job control / DEBUG trap interaction
 | 
| 307 |     opt_def.Add('_no_debug_trap')
 | 
| 308 |     # To implement ERR trap semantics - it's only run for the WHOLE pipeline,
 | 
| 309 |     # not each part (even the last part)
 | 
| 310 |     opt_def.Add('_no_err_trap')
 | 
| 311 | 
 | 
| 312 |     # shopt -s strict_arith, etc.
 | 
| 313 |     for name in _STRICT_OPTS:
 | 
| 314 |         opt_def.Add(name, groups=['strict:all', 'ysh:all'])
 | 
| 315 | 
 | 
| 316 |     #
 | 
| 317 |     # Options that enable YSH features
 | 
| 318 |     #
 | 
| 319 | 
 | 
| 320 |     for name in _UPGRADE_PARSE_OPTS:
 | 
| 321 |         opt_def.Add(name, groups=['ysh:upgrade', 'ysh:all'])
 | 
| 322 |     # shopt -s simple_word_eval, etc.
 | 
| 323 |     for name, default in _UPGRADE_RUNTIME_OPTS:
 | 
| 324 |         opt_def.Add(name, default=default, groups=['ysh:upgrade', 'ysh:all'])
 | 
| 325 | 
 | 
| 326 |     for name, default in _YSH_PARSE_OPTS:
 | 
| 327 |         opt_def.Add(name, default=default, groups=['ysh:all'])
 | 
| 328 |     for name, default in _YSH_RUNTIME_OPTS:
 | 
| 329 |         opt_def.Add(name, default=default, groups=['ysh:all'])
 | 
| 330 | 
 | 
| 331 |     opt_def.DoneWithImplementedOptions()
 | 
| 332 | 
 | 
| 333 |     # NO_OPS
 | 
| 334 | 
 | 
| 335 |     # Stubs for shopt -s xpg_echo, etc.
 | 
| 336 |     for name in _NO_OPS:
 | 
| 337 |         opt_def.Add(name, implemented=False)
 | 
| 338 | 
 | 
| 339 | 
 | 
| 340 | def All():
 | 
| 341 |     # type: () -> List[Option]
 | 
| 342 |     """Return a list of options with metadata.
 | 
| 343 | 
 | 
| 344 |     - Used by osh/builtin_pure.py to construct the arg spec.
 | 
| 345 |     - Used by frontend/lexer_gen.py to construct the lexer/matcher
 | 
| 346 |     """
 | 
| 347 |     return _OPTION_DEF.opts
 | 
| 348 | 
 | 
| 349 | 
 | 
| 350 | def ArraySize():
 | 
| 351 |     # type: () -> int
 | 
| 352 |     """Unused now, since we use opt_num::ARRAY_SIZE.
 | 
| 353 | 
 | 
| 354 |     We could get rid of unimplemented options and shrink the array.
 | 
| 355 |     """
 | 
| 356 |     return _OPTION_DEF.array_size
 | 
| 357 | 
 | 
| 358 | 
 | 
| 359 | def OptionDict():
 | 
| 360 |     # type: () -> Dict[str, int]
 | 
| 361 |     """For the slow path in frontend/match.py."""
 | 
| 362 |     return dict((opt.name, opt.index) for opt in _OPTION_DEF.opts)
 | 
| 363 | 
 | 
| 364 | 
 | 
| 365 | def ParseOptNames():
 | 
| 366 |     # type: () -> List[str]
 | 
| 367 |     """Used by core/optview*.py."""
 | 
| 368 |     return [opt.name for opt in _OPTION_DEF.opts if opt.is_parse]
 | 
| 369 | 
 | 
| 370 | 
 | 
| 371 | def ExecOptNames():
 | 
| 372 |     # type: () -> List[str]
 | 
| 373 |     """Used by core/optview*.py."""
 | 
| 374 |     return [opt.name for opt in _OPTION_DEF.opts if opt.is_exec]
 | 
| 375 | 
 | 
| 376 | 
 | 
| 377 | _OPTION_DEF = _OptionDef()
 | 
| 378 | 
 | 
| 379 | _Init(_OPTION_DEF)
 | 
| 380 | 
 | 
| 381 | # Sort by name because we print options.
 | 
| 382 | # TODO: for MEMBERSHIP queries, we could sort by the most common?  errexit
 | 
| 383 | # first?
 | 
| 384 | _SORTED = sorted(_OPTION_DEF.opts, key=lambda opt: opt.name)
 | 
| 385 | 
 | 
| 386 | PARSE_OPTION_NUMS = [opt.index for opt in _SORTED if opt.is_parse]
 | 
| 387 | 
 | 
| 388 | # Sorted because 'shopt -o -p' should be sorted, etc.
 | 
| 389 | VISIBLE_SHOPT_NUMS = [
 | 
| 390 |     opt.index for opt in _SORTED if opt.builtin == 'shopt' and opt.implemented
 | 
| 391 | ]
 | 
| 392 | 
 | 
| 393 | YSH_UPGRADE = [opt.index for opt in _SORTED if 'ysh:upgrade' in opt.groups]
 | 
| 394 | YSH_ALL = [opt.index for opt in _SORTED if 'ysh:all' in opt.groups]
 | 
| 395 | STRICT_ALL = [opt.index for opt in _SORTED if 'strict:all' in opt.groups]
 | 
| 396 | DEFAULT_TRUE = [opt.index for opt in _SORTED if opt.default]
 | 
| 397 | #print([opt.name for opt in _SORTED if opt.default])
 | 
| 398 | 
 | 
| 399 | META_OPTIONS = ['strict:all', 'ysh:upgrade',
 | 
| 400 |                 'ysh:all']  # Passed to flag parser
 | 
| 401 | 
 | 
| 402 | # For printing option names to stdout.  Wrapped by frontend/consts.
 | 
| 403 | OPTION_NAMES = dict((opt.index, opt.name) for opt in _SORTED)
 |