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

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