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

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