OILS / core / shell.py View on Github | oilshell.org

1135 lines, 728 significant
1"""
2core/shell.py -- Entry point for the shell interpreter.
3"""
4from __future__ import print_function
5
6from errno import ENOENT
7import time as time_
8
9from _devbuild.gen import arg_types
10from _devbuild.gen.option_asdl import option_i, builtin_i
11from _devbuild.gen.runtime_asdl import scope_e
12from _devbuild.gen.syntax_asdl import (loc, source, source_t, IntParamBox,
13 debug_frame, debug_frame_t)
14from _devbuild.gen.value_asdl import (value, value_e)
15from core import alloc
16from core import comp_ui
17from core import dev
18from core import error
19from core import executor
20from core import completion
21from core import main_loop
22from core import pyos
23from core import process
24from core import pyutil
25from core import state
26from core import ui
27from core import util
28from core import vm
29
30from frontend import args
31from frontend import flag_def # side effect: flags are defined!
32
33unused1 = flag_def
34from frontend import flag_util
35from frontend import location
36from frontend import reader
37from frontend import parse_lib
38
39from builtin import assign_osh
40from builtin import bracket_osh
41from builtin import completion_osh
42from builtin import completion_ysh
43from builtin import dirs_osh
44from builtin import error_ysh
45from builtin import hay_ysh
46from builtin import io_osh
47from builtin import io_ysh
48from builtin import json_ysh
49from builtin import meta_osh
50from builtin import misc_osh
51from builtin import module_ysh
52from builtin import printf_osh
53from builtin import process_osh
54from builtin import pure_osh
55from builtin import pure_ysh
56from builtin import readline_osh
57from builtin import read_osh
58from builtin import trap_osh
59
60from builtin import func_eggex
61from builtin import func_hay
62from builtin import func_misc
63
64from builtin import method_dict
65from builtin import method_io
66from builtin import method_list
67from builtin import method_other
68from builtin import method_str
69
70from osh import cmd_eval
71from osh import glob_
72from osh import history
73from osh import prompt
74from osh import sh_expr_eval
75from osh import split
76from osh import word_eval
77
78from mycpp import mops
79from mycpp import mylib
80from mycpp.mylib import print_stderr, log
81from pylib import os_path
82from tools import deps
83from tools import fmt
84from tools import ysh_ify
85from ysh import expr_eval
86
87unused2 = log
88
89import libc
90import posix_ as posix
91
92from typing import List, Dict, Optional, TYPE_CHECKING, cast
93if TYPE_CHECKING:
94 from frontend.py_readline import Readline
95
96if mylib.PYTHON:
97 try:
98 from _devbuild.gen import help_meta # type: ignore
99 except ImportError:
100 help_meta = None
101
102
103def _InitDefaultCompletions(cmd_ev, complete_builtin, comp_lookup):
104 # type: (cmd_eval.CommandEvaluator, completion_osh.Complete, completion.Lookup) -> None
105
106 # register builtins and words
107 complete_builtin.Run(cmd_eval.MakeBuiltinArgv(['-E', '-A', 'command']))
108 # register path completion
109 # Add -o filenames? Or should that be automatic?
110 complete_builtin.Run(cmd_eval.MakeBuiltinArgv(['-D', '-A', 'file']))
111
112
113def _CompletionDemo(comp_lookup):
114 # type: (completion.Lookup) -> None
115
116 # Something for fun, to show off. Also: test that you don't repeatedly hit
117 # the file system / network / coprocess.
118 A1 = completion.TestAction(['foo.py', 'foo', 'bar.py'], 0.0)
119 l = [] # type: List[str]
120 for i in xrange(0, 5):
121 l.append('m%d' % i)
122
123 A2 = completion.TestAction(l, 0.1)
124 C1 = completion.UserSpec([A1, A2], [], [], completion.DefaultPredicate(),
125 '', '')
126 comp_lookup.RegisterName('slowc', {}, C1)
127
128
129def SourceStartupFile(
130 fd_state, # type: process.FdState
131 rc_path, # type: str
132 lang, # type: str
133 parse_ctx, # type: parse_lib.ParseContext
134 cmd_ev, # type: cmd_eval.CommandEvaluator
135 errfmt, # type: ui.ErrorFormatter
136):
137 # type: (...) -> None
138
139 # Right now this is called when the shell is interactive. (Maybe it should
140 # be called on login_shel too.)
141 #
142 # Terms:
143 # - interactive shell: Roughly speaking, no args or -c, and isatty() is true
144 # for stdin and stdout.
145 # - login shell: Started from the top level, e.g. from init or ssh.
146 #
147 # We're not going to copy everything bash does because it's too complex, but
148 # for reference:
149 # https://www.gnu.org/software/bash/manual/bash.html#Bash-Startup-Files
150 # Bash also has --login.
151
152 try:
153 f = fd_state.Open(rc_path)
154 except (IOError, OSError) as e:
155 # TODO: Could warn about nonexistent explicit --rcfile?
156 if e.errno != ENOENT:
157 raise # Goes to top level. Handle this better?
158 return
159
160 arena = parse_ctx.arena
161 rc_line_reader = reader.FileLineReader(f, arena)
162 rc_c_parser = parse_ctx.MakeOshParser(rc_line_reader)
163
164 with alloc.ctx_SourceCode(arena, source.SourcedFile(rc_path, loc.Missing)):
165 # TODO: handle status, e.g. 2 for ParseError
166 unused = main_loop.Batch(cmd_ev, rc_c_parser, errfmt)
167
168 f.close()
169
170
171class ShellOptHook(state.OptHook):
172
173 def __init__(self, readline):
174 # type: (Optional[Readline]) -> None
175 self.readline = readline
176
177 def OnChange(self, opt0_array, opt_name, b):
178 # type: (List[bool], str, bool) -> bool
179 """This method is called whenever an option is changed.
180
181 Returns success or failure.
182 """
183 if opt_name == 'vi' or opt_name == 'emacs':
184 # TODO: Replace with a hook? Just like setting LANG= can have a hook.
185 if self.readline:
186 self.readline.parse_and_bind("set editing-mode " + opt_name)
187 else:
188 print_stderr(
189 "Warning: Can't set option %r because shell wasn't compiled with GNU readline"
190 % opt_name)
191 return False
192
193 # Invert: they are mutually exclusive!
194 if opt_name == 'vi':
195 opt0_array[option_i.emacs] = not b
196 elif opt_name == 'emacs':
197 opt0_array[option_i.vi] = not b
198
199 return True
200
201
202def _SetGlobalFunc(mem, name, func):
203 # type: (state.Mem, str, vm._Callable) -> None
204 assert isinstance(func, vm._Callable), func
205
206 # Note: no location info for builtin functions?
207 mem.SetNamed(location.LName(name), value.BuiltinFunc(func),
208 scope_e.GlobalOnly)
209
210
211def InitAssignmentBuiltins(
212 mem, # type: state.Mem
213 procs, # type: Dict[str, value.Proc]
214 errfmt, # type: ui.ErrorFormatter
215):
216 # type: (...) -> Dict[int, vm._AssignBuiltin]
217
218 assign_b = {} # type: Dict[int, vm._AssignBuiltin]
219
220 new_var = assign_osh.NewVar(mem, procs, errfmt)
221 assign_b[builtin_i.declare] = new_var
222 assign_b[builtin_i.typeset] = new_var
223 assign_b[builtin_i.local] = new_var
224
225 assign_b[builtin_i.export_] = assign_osh.Export(mem, errfmt)
226 assign_b[builtin_i.readonly] = assign_osh.Readonly(mem, errfmt)
227
228 return assign_b
229
230
231class ShellFiles(object):
232
233 def __init__(self, lang, home_dir, mem, flag):
234 # type: (str, str, state.Mem, arg_types.main) -> None
235 assert lang in ('osh', 'ysh'), lang
236 self.lang = lang
237 self.home_dir = home_dir
238 self.mem = mem
239 self.flag = flag
240
241 def _HistVar(self):
242 # type: () -> str
243 return 'HISTFILE' if self.lang == 'osh' else 'YSH_HISTFILE'
244
245 def _DefaultHistoryFile(self):
246 # type: () -> str
247 return os_path.join(self.home_dir,
248 '.local/share/oils/%s_history' % self.lang)
249
250 def InitAfterLoadingEnv(self):
251 # type: () -> None
252
253 hist_var = self._HistVar()
254 if self.mem.GetValue(hist_var).tag() == value_e.Undef:
255 # Note: if the directory doesn't exist, GNU readline ignores
256 state.SetGlobalString(self.mem, hist_var,
257 self._DefaultHistoryFile())
258
259 def HistoryFile(self):
260 # type: () -> Optional[str]
261 # TODO: In non-strict mode we should try to cast the HISTFILE value to a
262 # string following bash's rules
263
264 UP_val = self.mem.GetValue(self._HistVar())
265 if UP_val.tag() == value_e.Str:
266 val = cast(value.Str, UP_val)
267 return val.s
268 else:
269 # Note: if HISTFILE is an array, bash will return ${HISTFILE[0]}
270 return None
271 #return self._DefaultHistoryFile()
272
273 # TODO: can we recover line information here?
274 # might be useful to show where HISTFILE was set
275 #raise error.Strict("$HISTFILE should only ever be a string", loc.Missing)
276
277
278def Main(
279 lang, # type: str
280 arg_r, # type: args.Reader
281 environ, # type: Dict[str, str]
282 login_shell, # type: bool
283 loader, # type: pyutil._ResourceLoader
284 readline, # type: Optional[Readline]
285):
286 # type: (...) -> int
287 """The full shell lifecycle. Used by bin/osh and bin/ysh.
288
289 Args:
290 lang: 'osh' or 'ysh'
291 login_shell: Was - on argv[0]?
292 loader: to get help, version, grammar, etc.
293 readline: optional GNU readline
294 """
295 # Differences between osh and ysh:
296 # - oshrc vs yshrc
297 # - shopt -s ysh:all
298 # - Prompt
299 # - --help
300
301 argv0 = arg_r.Peek()
302 assert argv0 is not None
303 arg_r.Next()
304
305 assert lang in ('osh', 'ysh'), lang
306
307 try:
308 attrs = flag_util.ParseMore('main', arg_r)
309 except error.Usage as e:
310 print_stderr('%s usage error: %s' % (lang, e.msg))
311 return 2
312 flag = arg_types.main(attrs.attrs)
313
314 arena = alloc.Arena()
315 errfmt = ui.ErrorFormatter()
316
317 if flag.help:
318 util.HelpFlag(loader, '%s-usage' % lang, mylib.Stdout())
319 return 0
320 if flag.version:
321 util.VersionFlag(loader, mylib.Stdout())
322 return 0
323
324 if flag.tool == 'cat-em':
325 paths = arg_r.Rest()
326
327 status = 0
328 for p in paths:
329 try:
330 contents = loader.Get(p)
331 print(contents)
332 except (OSError, IOError):
333 print_stderr("cat-em: %r not found" % p)
334 status = 1
335 return status
336
337 debug_stack = [] # type: List[debug_frame_t]
338 if arg_r.AtEnd():
339 dollar0 = argv0
340 else:
341 dollar0 = arg_r.Peek() # the script name, or the arg after -c
342
343 frame0 = debug_frame.Main(dollar0)
344 debug_stack.append(frame0)
345
346 script_name = arg_r.Peek() # type: Optional[str]
347 arg_r.Next()
348 mem = state.Mem(dollar0, arg_r.Rest(), arena, debug_stack)
349
350 opt_hook = ShellOptHook(readline)
351 # Note: only MutableOpts needs mem, so it's not a true circular dep.
352 parse_opts, exec_opts, mutable_opts = state.MakeOpts(mem, opt_hook)
353 mem.exec_opts = exec_opts # circular dep
354 mutable_opts.Init()
355
356 version_str = pyutil.GetVersion(loader)
357 state.InitMem(mem, environ, version_str)
358
359 if attrs.show_options: # special case: sh -o
360 mutable_opts.ShowOptions([], False)
361 # removed return as sh -o does not return
362 # return 0
363
364 # Set these BEFORE processing flags, so they can be overridden.
365 if lang == 'ysh':
366 mutable_opts.SetAnyOption('ysh:all', True)
367
368 pure_osh.SetOptionsFromFlags(mutable_opts, attrs.opt_changes,
369 attrs.shopt_changes)
370
371 # feedback between runtime and parser
372 aliases = {} # type: Dict[str, str]
373
374 ysh_grammar = pyutil.LoadYshGrammar(loader)
375
376 if flag.do_lossless and not exec_opts.noexec():
377 raise error.Usage('--one-pass-parse requires noexec (-n)', loc.Missing)
378
379 # Tools always use one pass parse
380 # Note: osh --tool syntax-tree is like osh -n --one-pass-parse
381 do_lossless = True if len(flag.tool) else flag.do_lossless
382
383 parse_ctx = parse_lib.ParseContext(arena,
384 parse_opts,
385 aliases,
386 ysh_grammar,
387 do_lossless=do_lossless)
388
389 # Three ParseContext instances SHARE aliases.
390 comp_arena = alloc.Arena()
391 comp_arena.PushSource(source.Unused('completion'))
392 trail1 = parse_lib.Trail()
393 # do_lossless needs to be turned on to complete inside backticks. TODO:
394 # fix the issue where ` gets erased because it's not part of
395 # set_completer_delims().
396 comp_ctx = parse_lib.ParseContext(comp_arena,
397 parse_opts,
398 aliases,
399 ysh_grammar,
400 do_lossless=True)
401 comp_ctx.Init_Trail(trail1)
402
403 hist_arena = alloc.Arena()
404 hist_arena.PushSource(source.Unused('history'))
405 trail2 = parse_lib.Trail()
406 hist_ctx = parse_lib.ParseContext(hist_arena, parse_opts, aliases,
407 ysh_grammar)
408 hist_ctx.Init_Trail(trail2)
409
410 # Deps helps manages dependencies. These dependencies are circular:
411 # - cmd_ev and word_ev, arith_ev -- for command sub, arith sub
412 # - arith_ev and word_ev -- for $(( ${a} )) and $x$(( 1 ))
413 # - cmd_ev and builtins (which execute code, like eval)
414 # - prompt_ev needs word_ev for $PS1, which needs prompt_ev for @P
415 cmd_deps = cmd_eval.Deps()
416 cmd_deps.mutable_opts = mutable_opts
417
418 job_control = process.JobControl()
419 job_list = process.JobList()
420 fd_state = process.FdState(errfmt, job_control, job_list, mem, None, None)
421
422 my_pid = posix.getpid()
423
424 debug_path = ''
425 debug_dir = environ.get('OILS_DEBUG_DIR')
426 if flag.debug_file is not None:
427 # --debug-file takes precedence over OSH_DEBUG_DIR
428 debug_path = flag.debug_file
429 elif debug_dir is not None:
430 debug_path = os_path.join(debug_dir, '%d-osh.log' % my_pid)
431
432 if len(debug_path):
433 # This will be created as an empty file if it doesn't exist, or it could be
434 # a pipe.
435 try:
436 debug_f = util.DebugFile(
437 fd_state.OpenForWrite(debug_path)) # type: util._DebugFile
438 except (IOError, OSError) as e:
439 print_stderr("%s: Couldn't open %r: %s" %
440 (lang, debug_path, posix.strerror(e.errno)))
441 return 2
442 else:
443 debug_f = util.NullDebugFile()
444
445 if flag.xtrace_to_debug_file:
446 trace_f = debug_f
447 else:
448 trace_f = util.DebugFile(mylib.Stderr())
449
450 trace_dir = environ.get('OILS_TRACE_DIR', '')
451 dumps = environ.get('OILS_TRACE_DUMPS', '')
452 streams = environ.get('OILS_TRACE_STREAMS', '')
453 multi_trace = dev.MultiTracer(my_pid, trace_dir, dumps, streams, fd_state)
454
455 tracer = dev.Tracer(parse_ctx, exec_opts, mutable_opts, mem, trace_f,
456 multi_trace)
457 fd_state.tracer = tracer # circular dep
458
459 signal_safe = pyos.InitSignalSafe()
460 trap_state = trap_osh.TrapState(signal_safe)
461
462 waiter = process.Waiter(job_list, exec_opts, signal_safe, tracer)
463 fd_state.waiter = waiter
464
465 cmd_deps.debug_f = debug_f
466
467 now = time_.time()
468 iso_stamp = time_.strftime("%Y-%m-%d %H:%M:%S", time_.localtime(now))
469
470 argv_buf = mylib.BufWriter()
471 dev.PrintShellArgv(arg_r.argv, argv_buf)
472
473 debug_f.writeln('%s [%d] Oils started with argv %s' %
474 (iso_stamp, my_pid, argv_buf.getvalue()))
475 if len(debug_path):
476 debug_f.writeln('Writing logs to %r' % debug_path)
477
478 interp = environ.get('OILS_HIJACK_SHEBANG', '')
479 search_path = state.SearchPath(mem)
480 ext_prog = process.ExternalProgram(interp, fd_state, errfmt, debug_f)
481
482 splitter = split.SplitContext(mem)
483 # TODO: This is instantiation is duplicated in osh/word_eval.py
484 globber = glob_.Globber(exec_opts)
485
486 # This could just be OILS_TRACE_DUMPS='crash:argv0'
487 crash_dump_dir = environ.get('OILS_CRASH_DUMP_DIR', '')
488 cmd_deps.dumper = dev.CrashDumper(crash_dump_dir, fd_state)
489
490 comp_lookup = completion.Lookup()
491
492 # Various Global State objects to work around readline interfaces
493 compopt_state = completion.OptionState()
494
495 comp_ui_state = comp_ui.State()
496 prompt_state = comp_ui.PromptState()
497
498 # The login program is supposed to set $HOME
499 # https://superuser.com/questions/271925/where-is-the-home-environment-variable-set
500 # state.InitMem(mem) must happen first
501 tilde_ev = word_eval.TildeEvaluator(mem, exec_opts)
502 home_dir = tilde_ev.GetMyHomeDir()
503 if home_dir is None:
504 # TODO: print errno from getpwuid()
505 print_stderr("%s: Failed to get home dir from $HOME or getpwuid()" %
506 lang)
507 return 1
508
509 sh_files = ShellFiles(lang, home_dir, mem, flag)
510 sh_files.InitAfterLoadingEnv()
511
512 #
513 # Executor and Evaluators (are circularly dependent)
514 #
515
516 # Global proc namespace. Funcs are defined in the common variable
517 # namespace.
518 procs = {} # type: Dict[str, value.Proc]
519
520 builtins = {} # type: Dict[int, vm._Builtin]
521
522 # e.g. s->startswith()
523 methods = {} # type: Dict[int, Dict[str, vm._Callable]]
524
525 hay_state = hay_ysh.HayState()
526
527 shell_ex = executor.ShellExecutor(mem, exec_opts, mutable_opts, procs,
528 hay_state, builtins, search_path,
529 ext_prog, waiter, tracer, job_control,
530 job_list, fd_state, trap_state, errfmt)
531
532 arith_ev = sh_expr_eval.ArithEvaluator(mem, exec_opts, mutable_opts,
533 parse_ctx, errfmt)
534 bool_ev = sh_expr_eval.BoolEvaluator(mem, exec_opts, mutable_opts,
535 parse_ctx, errfmt)
536 expr_ev = expr_eval.ExprEvaluator(mem, mutable_opts, methods, splitter,
537 errfmt)
538 word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, mutable_opts,
539 tilde_ev, splitter, errfmt)
540
541 assign_b = InitAssignmentBuiltins(mem, procs, errfmt)
542 cmd_ev = cmd_eval.CommandEvaluator(mem, exec_opts, errfmt, procs, assign_b,
543 arena, cmd_deps, trap_state,
544 signal_safe)
545
546 # PromptEvaluator rendering is needed in non-interactive shells for @P.
547 prompt_ev = prompt.Evaluator(lang, version_str, parse_ctx, mem)
548 global_io = value.IO(cmd_ev, prompt_ev)
549 global_guts = value.Guts(None)
550
551 # Wire up circular dependencies.
552 vm.InitCircularDeps(arith_ev, bool_ev, expr_ev, word_ev, cmd_ev, shell_ex,
553 prompt_ev, global_io, tracer)
554
555 unsafe_arith = sh_expr_eval.UnsafeArith(mem, exec_opts, mutable_opts,
556 parse_ctx, arith_ev, errfmt)
557 vm.InitUnsafeArith(mem, word_ev, unsafe_arith)
558
559 #
560 # Initialize Built-in Procs
561 #
562
563 b = builtins # short alias for initialization
564
565 if mylib.PYTHON:
566 if help_meta:
567 help_data = help_meta.TopicMetadata()
568 else:
569 help_data = {} # minimal build
570 else:
571 help_data = help_meta.TopicMetadata()
572 b[builtin_i.help] = misc_osh.Help(lang, loader, help_data, errfmt)
573
574 # Interpreter state
575 b[builtin_i.set] = pure_osh.Set(mutable_opts, mem)
576 b[builtin_i.shopt] = pure_osh.Shopt(mutable_opts, cmd_ev)
577
578 b[builtin_i.hash] = pure_osh.Hash(search_path) # not really pure
579 b[builtin_i.trap] = trap_osh.Trap(trap_state, parse_ctx, tracer, errfmt)
580
581 b[builtin_i.shvar] = pure_ysh.Shvar(mem, search_path, cmd_ev)
582 b[builtin_i.ctx] = pure_ysh.Ctx(mem, cmd_ev)
583 b[builtin_i.push_registers] = pure_ysh.PushRegisters(mem, cmd_ev)
584
585 # Hay
586 b[builtin_i.hay] = hay_ysh.Hay(hay_state, mutable_opts, mem, cmd_ev)
587 b[builtin_i.haynode] = hay_ysh.HayNode_(hay_state, mem, cmd_ev)
588
589 # Interpreter introspection
590 b[builtin_i.type] = meta_osh.Type(procs, aliases, search_path, errfmt)
591 b[builtin_i.builtin] = meta_osh.Builtin(shell_ex, errfmt)
592 b[builtin_i.command] = meta_osh.Command(shell_ex, procs, aliases,
593 search_path)
594 # Part of YSH, but similar to builtin/command
595 b[builtin_i.runproc] = meta_osh.RunProc(shell_ex, procs, errfmt)
596
597 # Meta builtins
598 source_builtin = meta_osh.Source(parse_ctx, search_path, cmd_ev, fd_state,
599 tracer, errfmt, loader)
600 b[builtin_i.source] = source_builtin
601 b[builtin_i.dot] = source_builtin
602 b[builtin_i.eval] = meta_osh.Eval(parse_ctx, exec_opts, cmd_ev, tracer,
603 errfmt)
604
605 # Module builtins
606 modules = {} # type: Dict[str, bool]
607 b[builtin_i.module] = module_ysh.Module(modules, exec_opts, errfmt)
608 b[builtin_i.is_main] = module_ysh.IsMain(mem)
609 b[builtin_i.use] = module_ysh.Use(mem, errfmt)
610
611 # Errors
612 b[builtin_i.error] = error_ysh.Error()
613 b[builtin_i.failed] = error_ysh.Failed(mem)
614 b[builtin_i.boolstatus] = error_ysh.BoolStatus(shell_ex, errfmt)
615 b[builtin_i.try_] = error_ysh.Try(mutable_opts, mem, cmd_ev, shell_ex,
616 errfmt)
617
618 # Pure builtins
619 true_ = pure_osh.Boolean(0)
620 b[builtin_i.colon] = true_ # a "special" builtin
621 b[builtin_i.true_] = true_
622 b[builtin_i.false_] = pure_osh.Boolean(1)
623
624 b[builtin_i.alias] = pure_osh.Alias(aliases, errfmt)
625 b[builtin_i.unalias] = pure_osh.UnAlias(aliases, errfmt)
626
627 b[builtin_i.getopts] = pure_osh.GetOpts(mem, errfmt)
628
629 b[builtin_i.shift] = assign_osh.Shift(mem)
630 b[builtin_i.unset] = assign_osh.Unset(mem, procs, unsafe_arith, errfmt)
631
632 b[builtin_i.append] = pure_ysh.Append(mem, errfmt)
633
634 # test / [ differ by need_right_bracket
635 b[builtin_i.test] = bracket_osh.Test(False, exec_opts, mem, errfmt)
636 b[builtin_i.bracket] = bracket_osh.Test(True, exec_opts, mem, errfmt)
637
638 # Output
639 b[builtin_i.echo] = io_osh.Echo(exec_opts)
640 b[builtin_i.printf] = printf_osh.Printf(mem, parse_ctx, unsafe_arith,
641 errfmt)
642 b[builtin_i.write] = io_ysh.Write(mem, errfmt)
643 b[builtin_i.fopen] = io_ysh.Fopen(mem, cmd_ev)
644
645 # (pp output format isn't stable)
646 b[builtin_i.pp] = io_ysh.Pp(mem, errfmt, procs, arena)
647
648 # Input
649 b[builtin_i.cat] = io_osh.Cat() # for $(<file)
650 b[builtin_i.read] = read_osh.Read(splitter, mem, parse_ctx, cmd_ev, errfmt)
651
652 mapfile = io_osh.MapFile(mem, errfmt, cmd_ev)
653 b[builtin_i.mapfile] = mapfile
654 b[builtin_i.readarray] = mapfile
655
656 # Dirs
657 dir_stack = dirs_osh.DirStack()
658 b[builtin_i.cd] = dirs_osh.Cd(mem, dir_stack, cmd_ev, errfmt)
659 b[builtin_i.pushd] = dirs_osh.Pushd(mem, dir_stack, errfmt)
660 b[builtin_i.popd] = dirs_osh.Popd(mem, dir_stack, errfmt)
661 b[builtin_i.dirs] = dirs_osh.Dirs(mem, dir_stack, errfmt)
662 b[builtin_i.pwd] = dirs_osh.Pwd(mem, errfmt)
663
664 b[builtin_i.times] = misc_osh.Times()
665
666 b[builtin_i.json] = json_ysh.Json(mem, errfmt, False)
667 b[builtin_i.json8] = json_ysh.Json(mem, errfmt, True)
668
669 ### Process builtins
670 b[builtin_i.exec_] = process_osh.Exec(mem, ext_prog, fd_state, search_path,
671 errfmt)
672 b[builtin_i.umask] = process_osh.Umask()
673 b[builtin_i.ulimit] = process_osh.Ulimit()
674 b[builtin_i.wait] = process_osh.Wait(waiter, job_list, mem, tracer, errfmt)
675
676 b[builtin_i.jobs] = process_osh.Jobs(job_list)
677 b[builtin_i.fg] = process_osh.Fg(job_control, job_list, waiter)
678 b[builtin_i.bg] = process_osh.Bg(job_list)
679
680 # Could be in process_ysh
681 b[builtin_i.fork] = process_osh.Fork(shell_ex)
682 b[builtin_i.forkwait] = process_osh.ForkWait(shell_ex)
683
684 # Interactive builtins depend on readline
685 b[builtin_i.bind] = readline_osh.Bind(readline, errfmt)
686 b[builtin_i.history] = readline_osh.History(readline, sh_files, errfmt,
687 mylib.Stdout())
688
689 # Completion
690 spec_builder = completion_osh.SpecBuilder(cmd_ev, parse_ctx, word_ev,
691 splitter, comp_lookup, help_data,
692 errfmt)
693 complete_builtin = completion_osh.Complete(spec_builder, comp_lookup)
694 b[builtin_i.complete] = complete_builtin
695 b[builtin_i.compgen] = completion_osh.CompGen(spec_builder)
696 b[builtin_i.compopt] = completion_osh.CompOpt(compopt_state, errfmt)
697 b[builtin_i.compadjust] = completion_osh.CompAdjust(mem)
698
699 comp_ev = word_eval.CompletionWordEvaluator(mem, exec_opts, mutable_opts,
700 tilde_ev, splitter, errfmt)
701
702 comp_ev.arith_ev = arith_ev
703 comp_ev.expr_ev = expr_ev
704 comp_ev.prompt_ev = prompt_ev
705 comp_ev.CheckCircularDeps()
706
707 root_comp = completion.RootCompleter(comp_ev, mem, comp_lookup,
708 compopt_state, comp_ui_state,
709 comp_ctx, debug_f)
710 b[builtin_i.compexport] = completion_ysh.CompExport(root_comp)
711
712 #
713 # Initialize Builtin-in Methods
714 #
715
716 methods[value_e.Str] = {
717 'startsWith': method_str.HasAffix(method_str.START),
718 'endsWith': method_str.HasAffix(method_str.END),
719 'trim': method_str.Trim(method_str.START | method_str.END),
720 'trimStart': method_str.Trim(method_str.START),
721 'trimEnd': method_str.Trim(method_str.END),
722 'upper': method_str.Upper(),
723 'lower': method_str.Lower(),
724
725 # finds a substring, optional position to start at
726 'find': None,
727
728 # replace substring, OR an eggex
729 # takes count=3, the max number of replacements to do.
730 'replace': method_str.Replace(mem, expr_ev),
731
732 # Like Python's re.search, except we put it on the string object
733 # It's more consistent with Str->find(substring, pos=0)
734 # It returns value.Match() rather than an integer
735 'search': method_str.SearchMatch(method_str.SEARCH),
736
737 # like Python's re.match()
738 'leftMatch': method_str.SearchMatch(method_str.LEFT_MATCH),
739
740 # like Python's re.fullmatch(), not sure if we really need it
741 'fullMatch': None,
742 }
743 methods[value_e.Dict] = {
744 'get': None, # doesn't raise an error
745 'erase': None, # ensures it doesn't exist
746 'keys': method_dict.Keys(),
747 'values': method_dict.Values(),
748
749 # I think items() isn't as necessary because dicts are ordered?
750 # YSH code shouldn't use the List of Lists representation.
751
752 # could be d->tally() or d->increment(), but inc() is short
753 #
754 # call d->inc('mycounter')
755 # call d->inc('mycounter', 3)
756 'inc': None,
757
758 # call d->accum('mygroup', 'value')
759 'accum': None,
760 }
761 methods[value_e.List] = {
762 'reverse': method_list.Reverse(),
763 'append': method_list.Append(),
764 'extend': method_list.Extend(),
765 'pop': method_list.Pop(),
766 'insert': None, # insert object before index
767 'remove': None, # insert object before index
768 'indexOf': method_list.IndexOf(), # return first index of value, or -1
769 # Python list() has index(), which raises ValueError
770 # But this is consistent with Str->find(), and doesn't
771 # use exceptions
772 'join': func_misc.Join(), # both a method and a func
773 }
774
775 methods[value_e.Match] = {
776 'group': func_eggex.MatchMethod(func_eggex.G, expr_ev),
777 'start': func_eggex.MatchMethod(func_eggex.S, None),
778 'end': func_eggex.MatchMethod(func_eggex.E, None),
779 }
780
781 methods[value_e.IO] = {
782 # io->eval(myblock) is the functional version of eval (myblock)
783 # Should we also have expr->eval() instead of evalExpr?
784 'eval': method_io.Eval(),
785
786 # identical to command sub
787 'captureStdout': method_io.CaptureStdout(),
788 'promptVal': method_io.PromptVal(),
789 'time': method_io.Time(),
790 'strftime': method_io.Strftime(),
791 }
792
793 methods[value_e.Place] = {
794 # instead of setplace keyword
795 'setValue': method_other.SetValue(mem),
796 }
797
798 methods[value_e.Command] = {
799 # var x = ^(echo hi)
800 # Export source code and line number
801 # Useful for test frameworks and so forth
802 'export': None,
803 }
804
805 #
806 # Initialize Built-in Funcs
807 #
808
809 parse_hay = func_hay.ParseHay(fd_state, parse_ctx, errfmt)
810 eval_hay = func_hay.EvalHay(hay_state, mutable_opts, mem, cmd_ev)
811 hay_func = func_hay.HayFunc(hay_state)
812
813 _SetGlobalFunc(mem, 'parseHay', parse_hay)
814 _SetGlobalFunc(mem, 'evalHay', eval_hay)
815 _SetGlobalFunc(mem, '_hay', hay_func)
816
817 _SetGlobalFunc(mem, 'len', func_misc.Len())
818 _SetGlobalFunc(mem, 'type', func_misc.Type())
819 _SetGlobalFunc(mem, 'repeat', func_misc.Repeat())
820
821 g = func_eggex.MatchFunc(func_eggex.G, expr_ev, mem)
822 _SetGlobalFunc(mem, '_group', g)
823 _SetGlobalFunc(mem, '_match', g) # TODO: remove this backward compat alias
824 _SetGlobalFunc(mem, '_start', func_eggex.MatchFunc(func_eggex.S, None,
825 mem))
826 _SetGlobalFunc(mem, '_end', func_eggex.MatchFunc(func_eggex.E, None, mem))
827
828 _SetGlobalFunc(mem, 'join', func_misc.Join())
829 _SetGlobalFunc(mem, 'maybe', func_misc.Maybe())
830 _SetGlobalFunc(mem, 'evalExpr', func_misc.EvalExpr(expr_ev))
831
832 # type conversions
833 _SetGlobalFunc(mem, 'bool', func_misc.Bool())
834 _SetGlobalFunc(mem, 'int', func_misc.Int())
835 _SetGlobalFunc(mem, 'float', func_misc.Float())
836 _SetGlobalFunc(mem, 'str', func_misc.Str_())
837 _SetGlobalFunc(mem, 'list', func_misc.List_())
838 _SetGlobalFunc(mem, 'dict', func_misc.Dict_())
839
840 _SetGlobalFunc(mem, 'runes', func_misc.Runes())
841 _SetGlobalFunc(mem, 'encodeRunes', func_misc.EncodeRunes())
842 _SetGlobalFunc(mem, 'bytes', func_misc.Bytes())
843 _SetGlobalFunc(mem, 'encodeBytes', func_misc.EncodeBytes())
844
845 # TODO: This should be Python style splitting
846 _SetGlobalFunc(mem, 'split', func_misc.Split(splitter))
847 _SetGlobalFunc(mem, 'shSplit', func_misc.Split(splitter))
848
849 _SetGlobalFunc(mem, 'glob', func_misc.Glob(globber))
850 _SetGlobalFunc(mem, 'shvarGet', func_misc.Shvar_get(mem))
851 _SetGlobalFunc(mem, 'getVar', func_misc.GetVar(mem))
852 _SetGlobalFunc(mem, 'assert_', func_misc.Assert())
853
854 _SetGlobalFunc(mem, 'toJson8', func_misc.ToJson8(True))
855 _SetGlobalFunc(mem, 'toJson', func_misc.ToJson8(False))
856
857 _SetGlobalFunc(mem, 'fromJson8', func_misc.FromJson8(True))
858 _SetGlobalFunc(mem, 'fromJson', func_misc.FromJson8(False))
859
860 _SetGlobalFunc(mem, '_a2sp', func_misc.BashArrayToSparse())
861 _SetGlobalFunc(mem, '_d2sp', func_misc.DictToSparse())
862 _SetGlobalFunc(mem, '_opsp', func_misc.SparseOp())
863
864 mem.SetNamed(location.LName('_io'), global_io, scope_e.GlobalOnly)
865 mem.SetNamed(location.LName('_guts'), global_guts, scope_e.GlobalOnly)
866
867 #
868 # Is the shell interactive?
869 #
870
871 # History evaluation is a no-op if readline is None.
872 hist_ev = history.Evaluator(readline, hist_ctx, debug_f)
873
874 if flag.c is not None:
875 src = source.CFlag # type: source_t
876 line_reader = reader.StringLineReader(flag.c,
877 arena) # type: reader._Reader
878 if flag.i: # -c and -i can be combined
879 mutable_opts.set_interactive()
880
881 elif flag.i: # force interactive
882 src = source.Stdin(' -i')
883 line_reader = reader.InteractiveLineReader(arena, prompt_ev, hist_ev,
884 readline, prompt_state)
885 mutable_opts.set_interactive()
886
887 else:
888 if script_name is None:
889 if flag.headless:
890 src = source.Headless
891 line_reader = None # unused!
892 # Not setting '-i' flag for now. Some people's bashrc may want it?
893 else:
894 stdin_ = mylib.Stdin()
895 # --tool never starts a prompt
896 if len(flag.tool) == 0 and stdin_.isatty():
897 src = source.Interactive
898 line_reader = reader.InteractiveLineReader(
899 arena, prompt_ev, hist_ev, readline, prompt_state)
900 mutable_opts.set_interactive()
901 else:
902 src = source.Stdin('')
903 line_reader = reader.FileLineReader(stdin_, arena)
904 else:
905 src = source.MainFile(script_name)
906 try:
907 f = fd_state.Open(script_name)
908 except (IOError, OSError) as e:
909 print_stderr("%s: Couldn't open %r: %s" %
910 (lang, script_name, posix.strerror(e.errno)))
911 return 1
912 line_reader = reader.FileLineReader(f, arena)
913
914 # Pretend it came from somewhere else
915 if flag.location_str is not None:
916 src = source.Synthetic(flag.location_str)
917 assert line_reader is not None
918 location_start_line = mops.BigTruncate(flag.location_start_line)
919 if location_start_line != -1:
920 line_reader.SetLineOffset(location_start_line)
921
922 arena.PushSource(src)
923
924 # Calculate ~/.config/oils/oshrc or yshrc. Used for both -i and --headless
925 # We avoid cluttering the user's home directory. Some users may want to ln
926 # -s ~/.config/oils/oshrc ~/oshrc or ~/.oshrc.
927
928 # https://unix.stackexchange.com/questions/24347/why-do-some-applications-use-config-appname-for-their-config-data-while-other
929
930 config_dir = '.config/oils'
931 rc_paths = [] # type: List[str]
932 if not flag.norc and (flag.headless or exec_opts.interactive()):
933 # User's rcfile comes FIRST. Later we can add an 'after-rcdir' hook
934 rc_path = flag.rcfile
935 if rc_path is None:
936 rc_paths.append(
937 os_path.join(home_dir, '%s/%src' % (config_dir, lang)))
938 else:
939 rc_paths.append(rc_path)
940
941 # Load all files in ~/.config/oil/oshrc.d or oilrc.d
942 # This way "installers" can avoid mutating oshrc directly
943
944 rc_dir = flag.rcdir
945 if rc_dir is None:
946 rc_dir = os_path.join(home_dir, '%s/%src.d' % (config_dir, lang))
947
948 rc_paths.extend(libc.glob(os_path.join(rc_dir, '*')))
949 else:
950 if flag.rcfile is not None: # bash doesn't have this warning, but it's useful
951 print_stderr('%s warning: --rcfile ignored with --norc' % lang)
952 if flag.rcdir is not None:
953 print_stderr('%s warning: --rcdir ignored with --norc' % lang)
954
955 # Initialize even in non-interactive shell, for 'compexport'
956 _InitDefaultCompletions(cmd_ev, complete_builtin, comp_lookup)
957
958 if flag.headless:
959 state.InitInteractive(mem)
960 mutable_opts.set_redefine_proc_func()
961 mutable_opts.set_redefine_module()
962
963 # NOTE: rc files loaded AFTER _InitDefaultCompletions.
964 for rc_path in rc_paths:
965 with state.ctx_ThisDir(mem, rc_path):
966 try:
967 SourceStartupFile(fd_state, rc_path, lang, parse_ctx,
968 cmd_ev, errfmt)
969 except util.UserExit as e:
970 return e.status
971
972 loop = main_loop.Headless(cmd_ev, parse_ctx, errfmt)
973 try:
974 # TODO: What other exceptions happen here?
975 status = loop.Loop()
976 except util.UserExit as e:
977 status = e.status
978
979 # Same logic as interactive shell
980 mut_status = IntParamBox(status)
981 cmd_ev.MaybeRunExitTrap(mut_status)
982 status = mut_status.i
983
984 return status
985
986 # Note: headless mode above doesn't use c_parser
987 assert line_reader is not None
988 c_parser = parse_ctx.MakeOshParser(line_reader)
989
990 if exec_opts.interactive():
991 state.InitInteractive(mem)
992 # bash: 'set -o emacs' is the default only in the interactive shell
993 mutable_opts.set_emacs()
994 mutable_opts.set_redefine_proc_func()
995 mutable_opts.set_redefine_module()
996
997 if readline:
998 term_width = 0
999 if flag.completion_display == 'nice':
1000 try:
1001 term_width = libc.get_terminal_width()
1002 except (IOError, OSError): # stdin not a terminal
1003 pass
1004
1005 if term_width != 0:
1006 display = comp_ui.NiceDisplay(
1007 term_width, comp_ui_state, prompt_state, debug_f, readline,
1008 signal_safe) # type: comp_ui._IDisplay
1009 else:
1010 display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state,
1011 debug_f)
1012
1013 comp_ui.InitReadline(readline, sh_files.HistoryFile(), root_comp,
1014 display, debug_f)
1015
1016 _InitDefaultCompletions(cmd_ev, complete_builtin, comp_lookup)
1017 if flag.completion_demo:
1018 _CompletionDemo(comp_lookup)
1019
1020 else: # Without readline module
1021 display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state,
1022 debug_f)
1023
1024 process.InitInteractiveShell() # Set signal handlers
1025
1026 # The interactive shell leads a process group which controls the terminal.
1027 # It MUST give up the terminal afterward, otherwise we get SIGTTIN /
1028 # SIGTTOU bugs.
1029 with process.ctx_TerminalControl(job_control, errfmt):
1030
1031 # NOTE: rc files loaded AFTER _InitDefaultCompletions.
1032 for rc_path in rc_paths:
1033 with state.ctx_ThisDir(mem, rc_path):
1034 try:
1035 SourceStartupFile(fd_state, rc_path, lang, parse_ctx,
1036 cmd_ev, errfmt)
1037 except util.UserExit as e:
1038 return e.status
1039
1040 assert line_reader is not None
1041 line_reader.Reset() # After sourcing startup file, render $PS1
1042
1043 prompt_plugin = prompt.UserPlugin(mem, parse_ctx, cmd_ev, errfmt)
1044 try:
1045 status = main_loop.Interactive(flag, cmd_ev, c_parser, display,
1046 prompt_plugin, waiter, errfmt)
1047 except util.UserExit as e:
1048 status = e.status
1049
1050 mut_status = IntParamBox(status)
1051 cmd_ev.MaybeRunExitTrap(mut_status)
1052 status = mut_status.i
1053
1054 if readline:
1055 hist_file = sh_files.HistoryFile()
1056 if hist_file is not None:
1057 try:
1058 readline.write_history_file(hist_file)
1059 except (IOError, OSError):
1060 pass
1061
1062 return status
1063
1064 if flag.rcfile is not None: # bash doesn't have this warning, but it's useful
1065 print_stderr('%s warning: --rcfile ignored in non-interactive shell' %
1066 lang)
1067 if flag.rcdir is not None:
1068 print_stderr('%s warning: --rcdir ignored in non-interactive shell' %
1069 lang)
1070
1071 #
1072 # Tools that use the OSH/YSH parsing mode, etc.
1073 #
1074
1075 # flag.tool is '' if nothing is passed
1076 # osh --tool syntax-tree is equivalent to osh -n --one-pass-parse
1077 tool_name = 'syntax-tree' if exec_opts.noexec() else flag.tool
1078
1079 if len(tool_name):
1080 # Don't save tokens becaues it's slow
1081 if tool_name != 'syntax-tree':
1082 arena.SaveTokens()
1083
1084 try:
1085 node = main_loop.ParseWholeFile(c_parser)
1086 except error.Parse as e:
1087 errfmt.PrettyPrintError(e)
1088 return 2
1089
1090 if tool_name == 'syntax-tree':
1091 ui.PrintAst(node, flag)
1092
1093 elif tool_name == 'tokens':
1094 ysh_ify.PrintTokens(arena)
1095
1096 elif tool_name == 'lossless-cat': # for test/lossless.sh
1097 ysh_ify.LosslessCat(arena)
1098
1099 elif tool_name == 'fmt':
1100 fmt.Format(arena, node)
1101
1102 elif tool_name == 'test':
1103 raise AssertionError('TODO')
1104
1105 elif tool_name == 'ysh-ify':
1106 ysh_ify.Ysh_ify(arena, node)
1107
1108 elif tool_name == 'deps':
1109 if mylib.PYTHON:
1110 deps.Deps(node)
1111
1112 else:
1113 raise AssertionError(tool_name) # flag parser validated it
1114
1115 return 0
1116
1117 #
1118 # Run a shell script
1119 #
1120
1121 with state.ctx_ThisDir(mem, script_name):
1122 try:
1123 status = main_loop.Batch(cmd_ev,
1124 c_parser,
1125 errfmt,
1126 cmd_flags=cmd_eval.IsMainProgram)
1127 except util.UserExit as e:
1128 status = e.status
1129 mut_status = IntParamBox(status)
1130 cmd_ev.MaybeRunExitTrap(mut_status)
1131
1132 multi_trace.WriteDumps()
1133
1134 # NOTE: We haven't closed the file opened with fd_state.Open
1135 return mut_status.i