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

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