~andyc/oil

ee50bcd8b0a280c03f82f4b66259239a0d0aefe4 — Andy C 2 months ago 4f9a9ed
[translation] Translate exec / umask / trap builtins

This is a first pass -- some functions are stubbed out as
NotImplemented(), and we also have some 'if mylib.PYTHON' guards.

Now we can run Python's configure with bin/osh_eval.py!

Part of #1165.

Also add builtin_process to the build/app-deps filters, since it's not
ready to be translated.
M build/app-deps.sh => build/app-deps.sh +1 -0
@@ 65,6 65,7 @@ pgen2/parse.py
pylib/path_stat.py
oil_lang/objects.py
osh/bool_stat.py
osh/builtin_process.py
EOF

  wc -l $FILTER_DIR/filter-*

M build/app-deps/filter-translate.txt => build/app-deps/filter-translate.txt +1 -0
@@ 10,3 10,4 @@ pgen2/parse.py
pylib/path_stat.py
oil_lang/objects.py
osh/bool_stat.py
osh/builtin_process.py

M core/pyos.py => core/pyos.py +1 -1
@@ 23,7 23,7 @@ from typing import Optional, Tuple, List, Dict, cast, Any, TYPE_CHECKING

if TYPE_CHECKING:
  from core.comp_ui import _IDisplay
  from osh.builtin_process import _TrapHandler
  from osh.builtin_trap import _TrapHandler

_ = log


M core/shell.py => core/shell.py +6 -33
@@ 50,8 50,8 @@ from osh import builtin_meta
from osh import builtin_misc
from osh import builtin_lib
from osh import builtin_printf
from osh import builtin_process
from osh import builtin_pure
from osh import builtin_trap
from osh import cmd_eval
from osh import glob_
from osh import history


@@ 157,33 157,6 @@ class ShellOptHook(state.OptHook):
    return True


def AddProcess(
    b,  # type: Dict[int, vm._Builtin]
    mem,  # type: state.Mem
    shell_ex,  # type: vm._Executor
    ext_prog,  # type: process.ExternalProgram
    fd_state,  # type: process.FdState
    job_state,  # type: process.JobState
    waiter,  # type: process.Waiter
    tracer,  # type: dev.Tracer
    search_path,  # type: state.SearchPath
    errfmt  # type: ui.ErrorFormatter
    ):
    # type: (...) -> None

  # Process
  b[builtin_i.exec_] = builtin_process.Exec(mem, ext_prog, fd_state,
                                            search_path, errfmt)
  b[builtin_i.wait] = builtin_process.Wait(waiter, job_state, mem, tracer,
                                           errfmt)
  b[builtin_i.jobs] = builtin_process.Jobs(job_state)
  b[builtin_i.fg] = builtin_process.Fg(job_state, waiter)
  b[builtin_i.bg] = builtin_process.Bg(job_state)
  b[builtin_i.umask] = builtin_process.Umask()
  b[builtin_i.fork] = builtin_process.Fork(shell_ex)
  b[builtin_i.forkwait] = builtin_process.ForkWait(shell_ex)


def AddOil(b, mem, search_path, cmd_ev, errfmt, procs, arena):
  # type: (Dict[int, vm._Builtin], state.Mem, state.SearchPath, cmd_eval.CommandEvaluator, ui.ErrorFormatter, Dict[str, Proc], alloc.Arena) -> None
  b[builtin_i.append] = builtin_oil.Append(mem, errfmt)


@@ 435,8 408,8 @@ def Main(lang, arg_r, environ, login_shell, loader, line_input):
                       search_path, errfmt)
  shell_native.AddIO(builtins, mem, dir_stack, exec_opts, splitter, parse_ctx,
                     errfmt)
  AddProcess(builtins, mem, shell_ex, ext_prog, fd_state, job_state, waiter,
             tracer, search_path, errfmt)
  shell_native.AddProcess(builtins, mem, shell_ex, ext_prog, fd_state,
                          job_state, waiter, tracer, search_path, errfmt)

  builtins[builtin_i.help] = help_builtin



@@ 513,9 486,9 @@ def Main(lang, arg_r, environ, login_shell, loader, line_input):
  builtins[builtin_i.compopt] = builtin_comp.CompOpt(compopt_state, errfmt)
  builtins[builtin_i.compadjust] = builtin_comp.CompAdjust(mem)

  builtins[builtin_i.trap] = builtin_process.Trap(sig_state, cmd_deps.traps,
                                                  cmd_deps.trap_nodes,
                                                  parse_ctx, tracer, errfmt)
  builtins[builtin_i.trap] = builtin_trap.Trap(sig_state, cmd_deps.traps,
                                               cmd_deps.trap_nodes,
                                               parse_ctx, tracer, errfmt)

  # History evaluation is a no-op if line_input is None.
  hist_ev = history.Evaluator(line_input, hist_ctx, debug_f)

M core/shell_native.py => core/shell_native.py +46 -11
@@ 43,8 43,10 @@ from osh import builtin_bracket
from osh import builtin_meta
from osh import builtin_misc
from osh import builtin_printf
#from osh import builtin_process
from osh import builtin_process
from osh import builtin_process2  # can be translated
from osh import builtin_pure
from osh import builtin_trap
from osh import cmd_eval
from osh import prompt
from osh import sh_expr_eval


@@ 112,6 114,36 @@ def AddIO(b, mem, dir_stack, exec_opts, splitter, parse_ctx, errfmt):
  b[builtin_i.times] = builtin_misc.Times()


def AddProcess(
    b,  # type: Dict[int, vm._Builtin]
    mem,  # type: state.Mem
    shell_ex,  # type: vm._Executor
    ext_prog,  # type: process.ExternalProgram
    fd_state,  # type: process.FdState
    job_state,  # type: process.JobState
    waiter,  # type: process.Waiter
    tracer,  # type: dev.Tracer
    search_path,  # type: state.SearchPath
    errfmt  # type: ui.ErrorFormatter
    ):
    # type: (...) -> None

  # Process
  b[builtin_i.exec_] = builtin_process2.Exec(mem, ext_prog, fd_state,
                                            search_path, errfmt)
  b[builtin_i.umask] = builtin_process2.Umask()

  if mylib.PYTHON:
    b[builtin_i.wait] = builtin_process.Wait(waiter, job_state, mem, tracer,
                                             errfmt)
    b[builtin_i.jobs] = builtin_process.Jobs(job_state)
    b[builtin_i.fg] = builtin_process.Fg(job_state, waiter)
    b[builtin_i.bg] = builtin_process.Bg(job_state)

    b[builtin_i.fork] = builtin_process.Fork(shell_ex)
    b[builtin_i.forkwait] = builtin_process.ForkWait(shell_ex)


def AddMeta(builtins, shell_ex, mutable_opts, mem, procs, aliases, search_path,
            errfmt):
  # type: (Dict[int, vm._Builtin], vm._Executor, state.MutableOpts, state.Mem, Dict[str, Proc], Dict[str, str], state.SearchPath, ui.ErrorFormatter) -> None


@@ 367,8 399,14 @@ def Main(lang, arg_r, environ, login_shell, loader, line_input):
  builtins = {}  # type: Dict[int, vm._Builtin]
  modules = {}  # type: Dict[str, bool]

  shell_ex = executor.ShellExecutor(
      mem, exec_opts, mutable_opts, procs, hay_tree, builtins, search_path,
      ext_prog, waiter, tracer, job_state, fd_state, errfmt)

  AddPure(builtins, mem, procs, modules, mutable_opts, aliases, search_path, errfmt)
  AddIO(builtins, mem, dir_stack, exec_opts, splitter, parse_ctx, errfmt)
  AddProcess(builtins, mem, shell_ex, ext_prog, fd_state,
             job_state, waiter, tracer, search_path, errfmt)

  builtins[builtin_i.help] = help_builtin



@@ 386,9 424,6 @@ def Main(lang, arg_r, environ, login_shell, loader, line_input):
  cmd_ev = cmd_eval.CommandEvaluator(mem, exec_opts, errfmt, procs,
                                     assign_b, arena, cmd_deps)

  shell_ex = executor.ShellExecutor(
      mem, exec_opts, mutable_opts, procs, hay_tree, builtins, search_path,
      ext_prog, waiter, tracer, job_state, fd_state, errfmt)

  # PromptEvaluator rendering is needed in non-interactive shells for @P.
  prompt_ev = prompt.Evaluator(lang, version_str, parse_ctx, mem)


@@ 417,18 452,18 @@ def Main(lang, arg_r, environ, login_shell, loader, line_input):
  builtins[builtin_i.mapfile] = mapfile
  builtins[builtin_i.readarray] = mapfile

  #source_builtin = builtin_meta.Source(parse_ctx, search_path, cmd_ev,
                                       #fd_state, tracer, errfmt)
  #builtins[builtin_i.source] = source_builtin
  #builtins[builtin_i.dot] = source_builtin
  source_builtin = builtin_meta.Source(parse_ctx, search_path, cmd_ev,
                                       fd_state, tracer, errfmt)
  builtins[builtin_i.source] = source_builtin
  builtins[builtin_i.dot] = source_builtin

  AddMeta(builtins, shell_ex, mutable_opts, mem, procs, aliases, search_path,
          errfmt)
  AddBlock(builtins, mem, mutable_opts, dir_stack, cmd_ev, shell_ex, hay_tree, errfmt)

  #builtins[builtin_i.trap] = builtin_process.Trap(sig_state, cmd_deps.traps,
  #                                                cmd_deps.trap_nodes,
  #                                                parse_ctx, errfmt)
  builtins[builtin_i.trap] = builtin_trap.Trap(
      sig_state, cmd_deps.traps, cmd_deps.trap_nodes, parse_ctx, tracer,
      errfmt)

  if flag.c is not None:
    arena.PushSource(source.CFlag())

M cpp/leaky_core_pyos.h => cpp/leaky_core_pyos.h +11 -3
@@ 18,9 18,11 @@
#include "_build/cpp/syntax_asdl.h"
#include "leaky_time_.h"
#include "mycpp/mylib_leaky.h"
// has a member named errno.  That member can't be changed
// because it has to match the python error structure, which
// has an errno member.

// Hacky forward declaration
namespace builtin_trap {
class _TrapHandler;
};

namespace pyos {



@@ 130,6 132,12 @@ class SignalState {
  }
  void InitShell() {
  }
  void AddUserTrap(int sig_num, builtin_trap::_TrapHandler* handler) {
    NotImplemented();
  }
  void RemoveUserTrap(int sig_num) {
    NotImplemented();
  }
  int last_sig_num = 0;

  DISALLOW_COPY_AND_ASSIGN(SignalState)

M cpp/leaky_osh_eval_stubs.h => cpp/leaky_osh_eval_stubs.h +12 -44
@@ 12,11 12,13 @@
namespace vm {
class _Executor;
}

namespace word_eval {
class AbstractWordEvaluator;
}

namespace expr_eval {

class OilEvaluator {
 public:
  // TODO: Should return value_t


@@ 31,51 33,17 @@ class OilEvaluator {
};
}  // namespace expr_eval

namespace builtin_process {
class _TrapHandler {
 public:
  syntax_asdl::command_t* node;
};
}  // namespace builtin_process
namespace signal_def {

#if 0
namespace executor {
const int NO_SIGNAL = -1;

// can't inherit from incomplete type
// class ShellExecutor : public vm::_Executor {
class ShellExecutor {
 public:
  // overload
  int RunSimpleCommand(runtime_asdl::cmd_value__Argv* cmd_val, bool do_fork) {
    assert(0);
  }
  int RunSimpleCommand(runtime_asdl::cmd_value__Argv* cmd_val, bool do_fork,
                       bool call_procs) {
    assert(0);
  }
  int RunBackgroundJob(syntax_asdl::command_t* node) {
    assert(0);
  }
  int RunPipeline(syntax_asdl::command__Pipeline* node) {
    assert(0);
  }
  int RunSubshell(syntax_asdl::command__Subshell* node) {
    assert(0);
  }
  bool PushRedirects(List<runtime_asdl::redirect*>* redirects) {
    assert(0);
  }
  void PopRedirects() {
    assert(0);
  }
  Str* RunCommandSub(syntax_asdl::command_t* node) {
    assert(0);
  }
  Str* RunProcessSub(syntax_asdl::command_t* node, Id_t op_id) {
    assert(0);
  }
};
}  // namespace executor
#endif
inline List<Tuple2<Str*, int>*>* AllNames() {
  NotImplemented();
}

inline int GetNumber(Str* sig_spec) {
  NotImplemented();
}
}  // namespace signal_def

#endif  // OSH_EVAL_STUBS_H

M cpp/leaky_posix.cc => cpp/leaky_posix.cc +9 -2
@@ 7,12 7,19 @@
#include "leaky_posix.h"

#include <errno.h>
#include <fcntl.h>     // open
#include <sys/wait.h>  // WUNTRACED
#include <fcntl.h>      // open
#include <sys/stat.h>   // umask
#include <sys/types.h>  // umask
#include <sys/wait.h>   // WUNTRACED
#include <unistd.h>

namespace posix {

int umask(int mask) {
  // note: assuming mode_t fits in an int
  return ::umask(mask);
}

int open(Str* path, int flags, int perms) {
  mylib::Str0 path0(path);
  return ::open(path0.Get(), flags, perms);

M cpp/leaky_posix.h => cpp/leaky_posix.h +2 -0
@@ 15,6 15,8 @@

namespace posix {

int umask(int mask);

inline int access(Str* pathname, int mode) {
  // Are there any errno I care about?
  mylib::Str0 pathname0(pathname);

M frontend/flag_def.py => frontend/flag_def.py +2 -1
@@ 167,10 167,11 @@ HISTORY_SPEC.ShortFlag('-d', args.Int)
# osh/builtin_process.py
#

EXEC_SPEC = FlagSpec('exec')

WAIT_SPEC = FlagSpec('wait')
WAIT_SPEC.ShortFlag('-n')


TRAP_SPEC = FlagSpec('trap')
TRAP_SPEC.ShortFlag('-p')
TRAP_SPEC.ShortFlag('-l')

M frontend/signal_def.py => frontend/signal_def.py +3 -1
@@ 25,9 25,11 @@ def _MakeSignals():
  return names


NO_SIGNAL = -1

def GetNumber(sig_spec):
  # type: (str) -> int
  return _SIGNAL_NAMES.get(sig_spec)
  return _SIGNAL_NAMES.get(sig_spec, NO_SIGNAL)


_SIGNAL_NAMES = _MakeSignals()

M mycpp/mylib_leaky.cc => mycpp/mylib_leaky.cc +4 -0
@@ 359,6 359,10 @@ void BufWriter::format_d(int i) {
  len_ += len;
}

void BufWriter::format_o(int i) {
  NotImplemented();
}

// repr() calls this too
//
// TODO: This could be replaced with QSN?  The upper bound is greater there

M mycpp/mylib_leaky.h => mycpp/mylib_leaky.h +1 -0
@@ 1359,6 1359,7 @@ class BufWriter : public Writer {

  // strategy: snprintf() based on sizeof(int)
  void format_d(int i);
  void format_o(int i);
  void format_s(Str* s);
  void format_r(Str* s);  // formats with quotes


M osh/builtin_process.py => osh/builtin_process.py +5 -294
@@ 10,82 10,27 @@ import signal  # for calculating numbers

from _devbuild.gen import arg_types
from _devbuild.gen.runtime_asdl import (
    cmd_value, cmd_value__Argv,
    cmd_value__Argv,
    wait_status_e, wait_status__Proc, wait_status__Pipeline,
    wait_status__Cancelled,
)
from _devbuild.gen.syntax_asdl import source
from asdl import runtime
from core import alloc
from core import dev
from core import error
from core.pyerror import e_usage
from core import main_loop
from core.pyutil import stderr_line
from core import process  # W1_OK, W1_ECHILD
from core import pyos
from core import vm
from core.pyerror import log
from frontend import args
from frontend import flag_spec
from frontend import reader
from frontend import signal_def
from frontend import typed_args
from mycpp import mylib
from mycpp.mylib import iteritems, tagswitch
from mycpp.mylib import tagswitch

import posix_ as posix

from typing import List, Dict, Optional, Any, cast, TYPE_CHECKING
from typing import cast, TYPE_CHECKING
if TYPE_CHECKING:
  from _devbuild.gen.syntax_asdl import command_t
  from core.process import ExternalProgram, FdState, JobState, Waiter
  from core.state import Mem, SearchPath
  from core.process import JobState, Waiter
  from core.state import Mem
  from core.ui import ErrorFormatter
  from frontend.parse_lib import ParseContext


if mylib.PYTHON:
  EXEC_SPEC = flag_spec.FlagSpec('exec')


class Exec(vm._Builtin):

  def __init__(self, mem, ext_prog, fd_state, search_path, errfmt):
    # type: (Mem, ExternalProgram, FdState, SearchPath, ErrorFormatter) -> None
    self.mem = mem
    self.ext_prog = ext_prog
    self.fd_state = fd_state
    self.search_path = search_path
    self.errfmt = errfmt

  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int

    arg_r = args.Reader(cmd_val.argv, spids=cmd_val.arg_spids)
    arg_r.Next()  # skip 'exec'
    _ = args.Parse(EXEC_SPEC, arg_r)  # no flags now, but accepts --

    # Apply redirects in this shell.  # NOTE: Redirects were processed earlier.
    if arg_r.AtEnd():
      self.fd_state.MakePermanent()
      return 0

    environ = self.mem.GetExported()
    i = arg_r.i
    cmd = cmd_val.argv[i]
    argv0_path = self.search_path.CachedLookup(cmd)
    if argv0_path is None:
      self.errfmt.Print_('exec: %r not found' % cmd,
                         span_id=cmd_val.arg_spids[1])
      raise SystemExit(127)  # exec builtin never returns

    # shift off 'exec'
    c2 = cmd_value.Argv(cmd_val.argv[i:], cmd_val.arg_spids[i:],
                        cmd_val.typed_args)

    self.ext_prog.Exec(argv0_path, c2, environ)  # NEVER RETURNS
    assert False, "This line should never be reached" # makes mypy happy


class Wait(vm._Builtin):


@@ 280,240 225,6 @@ class Bg(vm._Builtin):
    raise error.Usage("isn't implemented")


class _TrapHandler(object):
  """A function that is called by Python's signal module.

  Similar to process.SubProgramThunk.

  TODO: In C++ we can't use this type of handling.  We cannot append to a
  garbage-colleted list inside a signal handler!

  Instead I think we need to append to a global array of size 1024 for the last
  signal number caught.

  Then in the main loop we will have RunPendingTraps() that iterates over this
  list, runs corresponding handlers, and then clears the list.
  """

  def __init__(self, node, nodes_to_run, sig_state, tracer):
    # type: (command_t, List[command_t], pyos.SignalState, dev.Tracer) -> None
    self.node = node
    self.nodes_to_run = nodes_to_run
    self.sig_state = sig_state
    self.tracer = tracer

  def __call__(self, sig_num, unused_frame):
    # type: (int, Any) -> None
    """For Python's signal module."""
    self.tracer.PrintMessage(
        'Received signal %d.  Will run handler in main loop' % sig_num)

    self.sig_state.last_sig_num = sig_num  # for interrupted 'wait'
    self.nodes_to_run.append(self.node)

  def __str__(self):
    # type: () -> str
    # Used by trap -p
    # TODO: Abbreviate with fmt.PrettyPrint?
    return '<Trap %s>' % self.node


def _GetSignalNumber(sig_spec):
  # type: (str) -> int

  # POSIX lists the numbers that are required.
  # http://pubs.opengroup.org/onlinepubs/9699919799/
  #
  # Added 13 for SIGPIPE because autoconf's 'configure' uses it!
  if sig_spec.strip() in ('1', '2', '3', '6', '9', '13', '14', '15'):
    return int(sig_spec)

  # INT is an alias for SIGINT
  if sig_spec.startswith('SIG'):
    sig_spec = sig_spec[3:]
  return signal_def.GetNumber(sig_spec)


_HOOK_NAMES = ['EXIT', 'ERR', 'RETURN', 'DEBUG']


# TODO:
#
# bash's default -p looks like this:
# trap -- '' SIGTSTP
# trap -- '' SIGTTIN
# trap -- '' SIGTTOU
#
# CPython registers different default handlers.  The C++ rewrite should make
# OVM match sh/bash more closely.

class Trap(vm._Builtin):
  def __init__(self, sig_state, traps, nodes_to_run, parse_ctx, tracer, errfmt):
    # type: (pyos.SignalState, Dict[str, _TrapHandler], List[command_t], ParseContext, dev.Tracer, ErrorFormatter) -> None
    self.sig_state = sig_state
    self.traps = traps
    self.nodes_to_run = nodes_to_run
    self.parse_ctx = parse_ctx
    self.arena = parse_ctx.arena
    self.tracer = tracer
    self.errfmt = errfmt

  def _ParseTrapCode(self, code_str):
    # type: (str) -> command_t
    """
    Returns:
      A node, or None if the code is invalid.
    """
    line_reader = reader.StringLineReader(code_str, self.arena)
    c_parser = self.parse_ctx.MakeOshParser(line_reader)

    # TODO: the SPID should be passed through argv.
    src = source.ArgvWord('trap', runtime.NO_SPID)
    with alloc.ctx_Location(self.arena, src):
      try:
        node = main_loop.ParseWholeFile(c_parser)
      except error.Parse as e:
        self.errfmt.PrettyPrintError(e)
        return None

    return node

  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int
    attrs, arg_r = flag_spec.ParseCmdVal('trap', cmd_val)
    arg = arg_types.trap(attrs.attrs)

    if arg.p:  # Print registered handlers
      for name, value in iteritems(self.traps):
        # The unit tests rely on this being one line.
        # bash prints a line that can be re-parsed.
        print('%s %s' % (name, value.__class__.__name__))

      return 0

    if arg.l:  # List valid signals and hooks
      for name in _HOOK_NAMES:
        print('   %s' % name)
      for name, int_val in signal_def.AllNames():
        print('%2d %s' % (int_val, name))

      return 0

    code_str = arg_r.ReadRequired('requires a code string')
    sig_spec, sig_spid = arg_r.ReadRequired2('requires a signal or hook name')

    # sig_key is NORMALIZED sig_spec: a signal number string or string hook
    # name.
    sig_key = None  # type: Optional[str]
    sig_num = None
    if sig_spec in _HOOK_NAMES:
      sig_key = sig_spec
    elif sig_spec == '0':  # Special case
      sig_key = 'EXIT'
    else:
      sig_num = _GetSignalNumber(sig_spec)
      if sig_num is not None:
        sig_key = str(sig_num)

    if sig_key is None:
      self.errfmt.Print_("Invalid signal or hook %r" % sig_spec,
                         span_id=cmd_val.arg_spids[2])
      return 1

    # NOTE: sig_spec isn't validated when removing handlers.
    if code_str == '-':
      if sig_key in _HOOK_NAMES:
        try:
          del self.traps[sig_key]
        except KeyError:
          pass
        return 0

      if sig_num is not None:
        try:
          del self.traps[sig_key]
        except KeyError:
          pass

        self.sig_state.RemoveUserTrap(sig_num)
        return 0

      raise AssertionError('Signal or trap')

    # Try parsing the code first.

    # TODO: If simple_trap is on (for oil:upgrade), then it must be a function
    # name?  And then you wrap it in 'try'?

    node = self._ParseTrapCode(code_str)
    if node is None:
      return 1  # ParseTrapCode() prints an error for us.

    # Register a hook.
    if sig_key in _HOOK_NAMES:
      if sig_key in ('ERR', 'RETURN', 'DEBUG'):
        stderr_line("osh warning: The %r hook isn't implemented", sig_spec)
      self.traps[sig_key] = _TrapHandler(node, self.nodes_to_run,
                                         self.sig_state, self.tracer)
      return 0

    # Register a signal.
    if sig_num is not None:
      handler = _TrapHandler(node, self.nodes_to_run, self.sig_state,
                             self.tracer)
      # For signal handlers, the traps dictionary is used only for debugging.
      self.traps[sig_key] = handler
      if sig_num in (signal.SIGKILL, signal.SIGSTOP):
        self.errfmt.Print_("Signal %r can't be handled" % sig_spec,
                           span_id=sig_spid)
        # Other shells return 0, but this seems like an obvious error
        return 1
      self.sig_state.AddUserTrap(sig_num, handler)
      return 0

    raise AssertionError('Signal or trap')

  # Example:
  # trap -- 'echo "hi  there" | wc ' SIGINT
  #
  # Then hit Ctrl-C.


class Umask(vm._Builtin):

  def __init__(self):
    # type: () -> None
    """Dummy constructor for mycpp."""
    pass

  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int

    argv = cmd_val.argv[1:]
    if len(argv) == 0:
      # umask() has a dumb API: you can't get it without modifying it first!
      # NOTE: dash disables interrupts around the two umask() calls, but that
      # shouldn't be a concern for us.  Signal handlers won't call umask().
      mask = posix.umask(0)
      posix.umask(mask)  #
      print('0%03o' % mask)  # octal format
      return 0

    if len(argv) == 1:
      a = argv[0]
      try:
        new_mask = int(a, 8)
      except ValueError:
        # NOTE: This happens if we have '8' or '9' in the input too.
        stderr_line("osh warning: umask with symbolic input isn't implemented")
        return 1
      else:
        posix.umask(new_mask)
        return 0

    e_usage('umask: unexpected arguments')


class Fork(vm._Builtin):

  def __init__(self, shell_ex):

A osh/builtin_process2.py => osh/builtin_process2.py +92 -0
@@ 0,0 1,92 @@
#!/usr/bin/env python2
"""
builtin_process2.py
"""
from __future__ import print_function

from _devbuild.gen.runtime_asdl import cmd_value, cmd_value__Argv

from core import vm
from core.pyerror import e_usage
from core.pyutil import stderr_line
from frontend import flag_spec

import posix_ as posix

from typing import TYPE_CHECKING
if TYPE_CHECKING:
  from core.process import ExternalProgram, FdState
  from core.state import Mem, SearchPath
  from core.ui import ErrorFormatter


class Exec(vm._Builtin):

  def __init__(self, mem, ext_prog, fd_state, search_path, errfmt):
    # type: (Mem, ExternalProgram, FdState, SearchPath, ErrorFormatter) -> None
    self.mem = mem
    self.ext_prog = ext_prog
    self.fd_state = fd_state
    self.search_path = search_path
    self.errfmt = errfmt

  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int
    _, arg_r = flag_spec.ParseCmdVal('exec', cmd_val)

    # Apply redirects in this shell.  # NOTE: Redirects were processed earlier.
    if arg_r.AtEnd():
      self.fd_state.MakePermanent()
      return 0

    environ = self.mem.GetExported()
    i = arg_r.i
    cmd = cmd_val.argv[i]
    argv0_path = self.search_path.CachedLookup(cmd)
    if argv0_path is None:
      self.errfmt.Print_('exec: %r not found' % cmd,
                         span_id=cmd_val.arg_spids[1])
      raise SystemExit(127)  # exec builtin never returns

    # shift off 'exec'
    c2 = cmd_value.Argv(cmd_val.argv[i:], cmd_val.arg_spids[i:],
                        cmd_val.typed_args)

    self.ext_prog.Exec(argv0_path, c2, environ)  # NEVER RETURNS
    # makes mypy and C++ compiler happy
    raise AssertionError('unreachable')


class Umask(vm._Builtin):

  def __init__(self):
    # type: () -> None
    """Dummy constructor for mycpp."""
    pass

  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int

    argv = cmd_val.argv[1:]
    if len(argv) == 0:
      # umask() has a dumb API: you can't get it without modifying it first!
      # NOTE: dash disables interrupts around the two umask() calls, but that
      # shouldn't be a concern for us.  Signal handlers won't call umask().
      mask = posix.umask(0)
      posix.umask(mask)  #
      print('0%03o' % mask)  # octal format
      return 0

    if len(argv) == 1:
      a = argv[0]
      try:
        new_mask = int(a, 8)
      except ValueError:
        # NOTE: This happens if we have '8' or '9' in the input too.
        stderr_line("osh warning: umask with symbolic input isn't implemented")
        return 1
      else:
        posix.umask(new_mask)
        return 0

    e_usage('umask: unexpected arguments')

A osh/builtin_trap.py => osh/builtin_trap.py +225 -0
@@ 0,0 1,225 @@
#!/usr/bin/env python2
"""
builtin_trap.py
"""
from __future__ import print_function

from signal import SIGKILL, SIGSTOP

from _devbuild.gen import arg_types
from _devbuild.gen.runtime_asdl import cmd_value__Argv
from _devbuild.gen.syntax_asdl import source
from asdl import runtime
from core import alloc
from core import dev
from core import error
from core import main_loop
from core import pyos
from core.pyutil import stderr_line
from core import vm
from frontend import flag_spec
from frontend import signal_def
from frontend import reader
from mycpp import mylib
from mycpp.mylib import iteritems

from typing import List, Dict, Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
  from _devbuild.gen.syntax_asdl import command_t
  from core.ui import ErrorFormatter
  from frontend.parse_lib import ParseContext


class _TrapHandler(object):
  """A function that is called by Python's signal module.

  Similar to process.SubProgramThunk.

  TODO: In C++ we can't use this type of handling.  We cannot append to a
  garbage-colleted list inside a signal handler!

  Instead I think we need to append to a global array of size 1024 for the last
  signal number caught.

  Then in the main loop we will have RunPendingTraps() that iterates over this
  list, runs corresponding handlers, and then clears the list.
  """

  def __init__(self, node, nodes_to_run, sig_state, tracer):
    # type: (command_t, List[command_t], pyos.SignalState, dev.Tracer) -> None
    self.node = node
    self.nodes_to_run = nodes_to_run
    self.sig_state = sig_state
    self.tracer = tracer

  def __call__(self, sig_num, unused_frame):
    # type: (int, Any) -> None
    """For Python's signal module."""
    self.tracer.PrintMessage(
        'Received signal %d.  Will run handler in main loop' % sig_num)

    self.sig_state.last_sig_num = sig_num  # for interrupted 'wait'
    self.nodes_to_run.append(self.node)


def _GetSignalNumber(sig_spec):
  # type: (str) -> int

  # POSIX lists the numbers that are required.
  # http://pubs.opengroup.org/onlinepubs/9699919799/
  #
  # Added 13 for SIGPIPE because autoconf's 'configure' uses it!
  if sig_spec.strip() in ('1', '2', '3', '6', '9', '13', '14', '15'):
    return int(sig_spec)

  # INT is an alias for SIGINT
  if sig_spec.startswith('SIG'):
    sig_spec = sig_spec[3:]
  return signal_def.GetNumber(sig_spec)


_HOOK_NAMES = ['EXIT', 'ERR', 'RETURN', 'DEBUG']


# bash's default -p looks like this:
# trap -- '' SIGTSTP
# trap -- '' SIGTTIN
# trap -- '' SIGTTOU
#
# CPython registers different default handlers.  The C++ rewrite should make
# OVM match sh/bash more closely.

# Example of trap:
# trap -- 'echo "hi  there" | wc ' SIGINT
#
# Then hit Ctrl-C.


class Trap(vm._Builtin):
  def __init__(self, sig_state, traps, nodes_to_run, parse_ctx, tracer, errfmt):
    # type: (pyos.SignalState, Dict[str, _TrapHandler], List[command_t], ParseContext, dev.Tracer, ErrorFormatter) -> None
    self.sig_state = sig_state
    self.traps = traps
    self.nodes_to_run = nodes_to_run
    self.parse_ctx = parse_ctx
    self.arena = parse_ctx.arena
    self.tracer = tracer
    self.errfmt = errfmt

  def _ParseTrapCode(self, code_str):
    # type: (str) -> command_t
    """
    Returns:
      A node, or None if the code is invalid.
    """
    line_reader = reader.StringLineReader(code_str, self.arena)
    c_parser = self.parse_ctx.MakeOshParser(line_reader)

    # TODO: the SPID should be passed through argv.
    src = source.ArgvWord('trap', runtime.NO_SPID)
    with alloc.ctx_Location(self.arena, src):
      try:
        node = main_loop.ParseWholeFile(c_parser)
      except error.Parse as e:
        self.errfmt.PrettyPrintError(e)
        return None

    return node

  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int
    attrs, arg_r = flag_spec.ParseCmdVal('trap', cmd_val)
    arg = arg_types.trap(attrs.attrs)

    if arg.p:  # Print registered handlers
      if mylib.PYTHON:
        for name, value in iteritems(self.traps):
          # The unit tests rely on this being one line.
          # bash prints a line that can be re-parsed.
          print('%s %s' % (name, value.__class__.__name__))

      return 0

    if arg.l:  # List valid signals and hooks
      for name in _HOOK_NAMES:
        print('   %s' % name)
      if mylib.PYTHON:
        for name, int_val in signal_def.AllNames():
          print('%2d %s' % (int_val, name))

      return 0

    code_str = arg_r.ReadRequired('requires a code string')
    sig_spec, sig_spid = arg_r.ReadRequired2('requires a signal or hook name')

    # sig_key is NORMALIZED sig_spec: a signal number string or string hook
    # name.
    sig_key = None  # type: Optional[str]
    sig_num = signal_def.NO_SIGNAL

    if sig_spec in _HOOK_NAMES:
      sig_key = sig_spec
    elif sig_spec == '0':  # Special case
      sig_key = 'EXIT'
    else:
      sig_num = _GetSignalNumber(sig_spec)
      if sig_num != signal_def.NO_SIGNAL:
        sig_key = str(sig_num)

    if sig_key is None:
      self.errfmt.Print_("Invalid signal or hook %r" % sig_spec,
                         span_id=cmd_val.arg_spids[2])
      return 1

    # NOTE: sig_spec isn't validated when removing handlers.
    if code_str == '-':
      if sig_key in _HOOK_NAMES:
        try:
          del self.traps[sig_key]
        except KeyError:
          pass
        return 0

      if sig_num != signal_def.NO_SIGNAL:
        try:
          del self.traps[sig_key]
        except KeyError:
          pass

        self.sig_state.RemoveUserTrap(sig_num)
        return 0

      raise AssertionError('Signal or trap')

    # Try parsing the code first.

    # TODO: If simple_trap is on (for oil:upgrade), then it must be a function
    # name?  And then you wrap it in 'try'?

    node = self._ParseTrapCode(code_str)
    if node is None:
      return 1  # ParseTrapCode() prints an error for us.

    # Register a hook.
    if sig_key in _HOOK_NAMES:
      if sig_key in ('ERR', 'RETURN', 'DEBUG'):
        stderr_line("osh warning: The %r hook isn't implemented", sig_spec)
      self.traps[sig_key] = _TrapHandler(node, self.nodes_to_run,
                                         self.sig_state, self.tracer)
      return 0

    # Register a signal.
    if sig_num != signal_def.NO_SIGNAL:
      handler = _TrapHandler(node, self.nodes_to_run, self.sig_state,
                             self.tracer)
      # For signal handlers, the traps dictionary is used only for debugging.
      self.traps[sig_key] = handler
      if sig_num in (SIGKILL, SIGSTOP):
        self.errfmt.Print_("Signal %r can't be handled" % sig_spec,
                           span_id=sig_spid)
        # Other shells return 0, but this seems like an obvious error
        return 1
      self.sig_state.AddUserTrap(sig_num, handler)
      return 0

    raise AssertionError('Signal or trap')

M osh/cmd_eval.py => osh/cmd_eval.py +3 -3
@@ 87,7 87,7 @@ if TYPE_CHECKING:
  from core.vm import _Executor, _AssignBuiltin
  from oil_lang import expr_eval
  from osh import word_eval
  from osh import builtin_process
  from osh import builtin_trap

# flags for main_loop.Batch, ExecuteAndCatch.  TODO: Should probably in
# ExecuteAndCatch, along with SetValue() flags.


@@ 116,7 116,7 @@ class Deps(object):
    self.debug_f = None       # type: util._DebugFile

    # signal/hook name -> handler
    self.traps = None         # type: Dict[str, builtin_process._TrapHandler]
    self.traps = None         # type: Dict[str, builtin_trap._TrapHandler]
    # appended to by signal handlers
    self.trap_nodes = None    # type: List[command_t]



@@ 1428,7 1428,7 @@ class CommandEvaluator(object):
  def RunPendingTraps(self):
    # type: () -> None

    # See osh/builtin_process.py _TrapHandler for the code that appends to this
    # See osh/builtin_trap.py _TrapHandler for the code that appends to this
    # list.
    if len(self.trap_nodes):
      # Make a copy and clear it so we don't cause an infinite loop.