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

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