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

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