OILS / core / sh_init.py View on Github | oils.pub

336 lines, 148 significant
1from __future__ import print_function
2
3from _devbuild.gen.runtime_asdl import scope_e
4from _devbuild.gen.value_asdl import value, value_e, value_t
5from core.error import e_die
6from core import pyos
7from core import pyutil
8from core import optview
9from core import state
10from frontend import location
11from mycpp.mylib import iteritems, tagswitch, log
12from osh import split
13from pylib import os_path
14
15import libc
16import posix_ as posix
17
18from typing import List, Dict, Optional, cast, TYPE_CHECKING
19if TYPE_CHECKING:
20 from _devbuild.gen import arg_types
21
22_ = log
23
24
25class EnvConfig(object):
26 """Manage shell config from the environment, for OSH and YSH.
27
28 Variables managed:
29
30 PATH aka ENV.PATH - where to look for executables
31 PS1 - how to print the prompt
32 HISTFILE YSH_HISTFILE - where to read/write command history
33 HOME - for ~ expansion (default not set)
34
35 Features TODO
36
37 - On-demand BASHPID
38 - io.thisPid() - is BASHPID
39 - io.pid() - is $$
40 - Init-once UID EUID PPID
41 - maybe this should be a separate Funcs class?
42 - io.uid() io.euid() io.ppid()
43 """
44
45 def __init__(self, mem, defaults):
46 # type: (state.Mem, Dict[str, value_t]) -> None
47 self.mem = mem
48 self.exec_opts = mem.exec_opts
49 self.defaults = defaults
50 # -1: not yet captured, 0: False (OSH), 1: True (YSH)
51 # Lazy because exec_opts is None when Mem.__init__ creates EnvConfig
52 self._env_obj_at_startup = -1 # type: int
53
54 def GetVal(self, var_name):
55 # type: (str) -> value_t
56 """
57 YSH: Look at ENV.PATH, and then __defaults__.PATH
58 OSH: Look at $PATH
59 """
60 if self._env_obj_at_startup == -1:
61 self._env_obj_at_startup = 1 if self.mem.exec_opts.env_obj() else 0
62
63 use_env_obj = self.mem.exec_opts.env_obj()
64 # Prompt variables use startup value to fix #2367: prompt shouldn't
65 # disappear when 'shopt --unset ysh:all' or 'shopt --set ysh:all'
66 if var_name in ('PS1', 'PS2', 'PS3', 'PS4'):
67 use_env_obj = bool(self._env_obj_at_startup)
68
69 if use_env_obj: # e.g. $[ENV.PATH]
70
71 val = self.mem.env_dict.get(var_name)
72 if val is None:
73 val = self.defaults.get(var_name)
74
75 if val is None:
76 return value.Undef
77
78 #log('**ENV obj val = %s', val)
79
80 else: # e.g. $PATH
81 val = self.mem.GetValue(var_name)
82
83 return val
84
85 def Get(self, var_name):
86 # type: (str) -> Optional[str]
87 """
88 Like GetVal(), but returns a strin, or None
89 """
90 val = self.GetVal(var_name)
91 if val.tag() != value_e.Str:
92 return None
93 return cast(value.Str, val).s
94
95 def SetDefault(self, var_name, s):
96 # type: (str, str) -> None
97 """
98 OSH: Set HISTFILE var, which is read by GetVal()
99 YSH: Set __defaults__.YSH_HISTFILE, which is also read by GetVal()
100 """
101 if self.mem.exec_opts.env_obj(): # e.g. $[ENV.PATH]
102 self.mem.defaults[var_name] = value.Str(s)
103 else:
104 state.SetGlobalString(self.mem, var_name, s)
105
106
107class ShellFiles(object):
108
109 def __init__(self, lang, home_dir, mem, flag):
110 # type: (str, str, state.Mem, arg_types.main) -> None
111 assert lang in ('osh', 'ysh'), lang
112 self.lang = lang
113 self.home_dir = home_dir
114 self.mem = mem
115 self.flag = flag
116
117 self.init_done = False
118
119 def HistVar(self):
120 # type: () -> str
121 return 'HISTFILE' if self.lang == 'osh' else 'YSH_HISTFILE'
122
123 def DefaultHistoryFile(self):
124 # type: () -> str
125 return os_path.join(self.home_dir,
126 '.local/share/oils/%s_history' % self.lang)
127
128 def HistoryFile(self):
129 # type: () -> Optional[str]
130 assert self.init_done
131
132 return self.mem.env_config.Get(self.HistVar())
133
134
135def GetWorkingDir():
136 # type: () -> str
137 """Fallback for pwd builtin and $PWD when there's no 'cd' and no inherited $PWD."""
138 try:
139 return posix.getcwd()
140 except (IOError, OSError) as e:
141 e_die("Can't determine the working dir: %s" % pyutil.strerror(e))
142
143
144# This was derived from bash --norc -c 'argv "$COMP_WORDBREAKS".
145# Python overwrites this to something Python-specific in Modules/readline.c, so
146# we have to set it back!
147# Used in both core/competion.py and osh/state.py
148_READLINE_DELIMS = ' \t\n"\'><=;|&(:'
149
150
151def InitDefaultVars(mem, argv):
152 # type: (state.Mem, List[str]) -> None
153
154 # Problem: if you do shopt --set ysh:upgrade, the variable won't be
155 # initialized. So this is not different than lang == 'ysh'
156 if mem.exec_opts.init_ysh_globals(): # YSH init
157 # mem is initialized with a global frame
158 mem.var_stack[0]['ARGV'] = state._MakeArgvCell(argv)
159
160 # These 3 are special, can't be changed
161 state.SetGlobalString(mem, 'UID', str(posix.getuid()))
162 state.SetGlobalString(mem, 'EUID', str(posix.geteuid()))
163 state.SetGlobalString(mem, 'PPID', str(posix.getppid()))
164
165 # For getopts builtin - meant to be read, not changed
166 state.SetGlobalString(mem, 'OPTIND', '1')
167
168 # These can be changed. Could go AFTER environment, e.g. in
169 # InitVarsAfterEnv().
170
171 # Default value; user may unset it.
172 # $ echo -n "$IFS" | python -c 'import sys;print repr(sys.stdin.read())'
173 # ' \t\n'
174 state.SetGlobalString(mem, 'IFS', split.DEFAULT_IFS)
175
176 state.SetGlobalString(mem, 'HOSTNAME', libc.gethostname())
177
178 # In bash, this looks like 'linux-gnu', 'linux-musl', etc. Scripts test
179 # for 'darwin' and 'freebsd' too. They generally don't like at 'gnu' or
180 # 'musl'. We don't have that info, so just make it 'linux'.
181 state.SetGlobalString(mem, 'OSTYPE', pyos.OsType())
182
183 # When xtrace_rich is off, this is just like '+ ', the shell default
184 state.SetGlobalString(mem, 'PS4',
185 '${SHX_indent}${SHX_punct}${SHX_pid_str} ')
186
187 # bash-completion uses this. Value copied from bash. It doesn't integrate
188 # with 'readline' yet.
189 state.SetGlobalString(mem, 'COMP_WORDBREAKS', _READLINE_DELIMS)
190
191 # TODO on $HOME: bash sets it if it's a login shell and not in POSIX mode!
192 # if (login_shell == 1 && posixly_correct == 0)
193 # set_home_var ();
194
195
196def CopyVarsFromEnv(exec_opts, environ, mem):
197 # type: (optview.Exec, Dict[str, str], state.Mem) -> None
198
199 # POSIX shell behavior: env vars become exported global vars
200 if not exec_opts.no_exported():
201 # This is the way dash and bash work -- at startup, they turn everything in
202 # 'environ' variable into shell variables. Bash has an export_env
203 # variable. Dash has a loop through environ in init.c
204 for n, v in iteritems(environ):
205 mem.SetNamed(location.LName(n),
206 value.Str(v),
207 scope_e.GlobalOnly,
208 flags=state.SetExport)
209
210 # YSH behavior: env vars go in ENV dict, not exported vars. Note that
211 # ysh:upgrade can have BOTH ENV and exported vars. It's OK if they're on
212 # at the same time.
213 if exec_opts.env_obj():
214 # This is for invoking bin/ysh
215 # If you run bin/osh, then exec_opts.env_obj() will be FALSE at this point.
216 # When you write shopt --set ysh:all or ysh:upgrade, then the shopt
217 # builtin will call MaybeInitEnvDict()
218 mem.MaybeInitEnvDict(environ)
219
220
221def InitVarsAfterEnv(mem, mutable_opts):
222 # type: (state.Mem, state.MutableOpts) -> None
223
224 # If PATH SHELLOPTS PWD are not in environ, then initialize them.
225 s = mem.env_config.Get('PATH')
226 if s is None:
227 # Setting PATH to these four dirs match busybox ash. zsh and mksh only
228 # do /bin:/usr/bin while bash and dash add {,/usr/,/usr/local}/{bin,sbin}
229 # The default PATH in busybox ash is defined here:
230 # busybox https://github.com/mirror/busybox/blob/371fe9f71d445d18be28c82a2a6d82115c8af19d/include/libbb.h#L2303
231 # The default PATH in bash is defined here:
232 # https://github.com/bminor/bash/blob/a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b/config-top.h#L61
233 mem.env_config.SetDefault('PATH', '/sbin:/usr/sbin:/bin:/usr/bin')
234
235 if mem.exec_opts.no_init_globals():
236 # YSH initialization
237 mem.SetPwd(GetWorkingDir())
238
239 # TODO: YSH can use cross-process tracing with SHELLOPTS, BASHOPTS, and
240 # OILS_OPTS?
241 # Or at least the xtrace stuff should be in OILS_OPTS. Bash has a
242 # quirk where these env vars turn options ON, but they don't turn
243 # options OFF. So it is perhaps not a great mechanism.
244 else:
245 # OSH initialization
246 shellopts = mem.GetValue('SHELLOPTS')
247 UP_shellopts = shellopts
248 with tagswitch(shellopts) as case:
249 if case(value_e.Str):
250 shellopts = cast(value.Str, UP_shellopts)
251 mutable_opts.InitFromEnv(shellopts.s)
252 elif case(value_e.Undef):
253 # If it's not in the environment, construct the string
254 state.SetGlobalString(mem, 'SHELLOPTS',
255 mutable_opts.ShelloptsString())
256 else:
257 raise AssertionError()
258
259 # Mark it readonly, like bash
260 mem.SetNamed(location.LName('SHELLOPTS'),
261 None,
262 scope_e.GlobalOnly,
263 flags=state.SetReadOnly)
264
265 # NOTE: bash also has BASHOPTS
266
267 our_pwd = None # type: Optional[str]
268 val = mem.GetValue('PWD')
269 if val.tag() == value_e.Str:
270 env_pwd = cast(value.Str, val).s
271 # POSIX rule: PWD is inherited if it's an absolute path that corresponds to '.'
272 if env_pwd.startswith('/') and pyos.IsSameFile(env_pwd, '.'):
273 our_pwd = env_pwd
274
275 # POSIX: Otherwise, recalculate it
276 if our_pwd is None:
277 our_pwd = GetWorkingDir()
278
279 # It's EXPORTED, even if it's not set. bash and dash both do this:
280 # env -i -- dash -c env
281 mem.SetNamed(location.LName('PWD'),
282 value.Str(our_pwd),
283 scope_e.GlobalOnly,
284 flags=state.SetExport)
285
286 # Set a MUTABLE GLOBAL that's SEPARATE from $PWD. It's used by the 'pwd'
287 # builtin, and it can't be modified by users.
288 mem.SetPwd(our_pwd)
289
290
291def InitInteractive(mem, sh_files, lang):
292 # type: (state.Mem, ShellFiles, str) -> None
293 """Initialization that's only done in the interactive/headless shell."""
294
295 ps1_str = mem.env_config.Get('PS1')
296 if ps1_str is None:
297 mem.env_config.SetDefault('PS1', r'\s-\v\$ ')
298 else:
299 if lang == 'ysh':
300 # If this is bin/ysh, and we got a plain PS1, then prepend 'ysh ' to PS1
301 mem.env_dict['PS1'] = value.Str('ysh ' + ps1_str)
302
303 hist_var = sh_files.HistVar()
304 hist_str = mem.env_config.Get(hist_var)
305 if hist_str is None:
306 mem.env_config.SetDefault(hist_var, sh_files.DefaultHistoryFile())
307
308 sh_files.init_done = True # sanity check before using sh_files
309
310
311def InitBuiltins(mem, version_str, defaults):
312 # type: (state.Mem, str, Dict[str, value_t]) -> None
313 """Initialize memory with shell defaults.
314
315 Other interpreters could have different builtin variables.
316 """
317 # TODO: REMOVE this legacy. ble.sh checks it!
318 mem.builtins['OIL_VERSION'] = value.Str(version_str)
319
320 mem.builtins['OILS_VERSION'] = value.Str(version_str)
321
322 mem.builtins['__defaults__'] = value.Dict(defaults)
323
324 # The source builtin understands '///' to mean "relative to embedded stdlib"
325 mem.builtins['LIB_OSH'] = value.Str('///osh')
326 mem.builtins['LIB_YSH'] = value.Str('///ysh')
327
328 # - C spells it NAN
329 # - JavaScript spells it NaN
330 # - Python 2 has float('nan'), while Python 3 has math.nan.
331 #
332 # - libc prints the strings 'nan' and 'inf'
333 # - Python 3 prints the strings 'nan' and 'inf'
334 # - JavaScript prints 'NaN' and 'Infinity', which is more stylized
335 mem.builtins['NAN'] = value.Float(pyutil.nan())
336 mem.builtins['INFINITY'] = value.Float(pyutil.infinity())