OILS / core / sh_init.py View on Github | oilshell.org

371 lines, 158 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 tagswitch, iteritems, log
12from osh import split
13from pylib import os_path
14
15import libc
16import posix_ as posix
17
18from typing import Dict, Optional, cast, TYPE_CHECKING
19if TYPE_CHECKING:
20 from _devbuild.gen import arg_types
21
22_ = log
23
24
25class EnvConfig(object):
26 """Define a string config var read from the environment.
27
28 And it's default.
29
30 In OSH, it will appear as $PS1 or $PATH or $PWD. You can't see the
31 default.
32
33 In YSH, it will appear as ENV.PS1 and __default__.PS1. I guess __default__
34 can be a Dict or Obj.
35
36 Usage:
37
38 env_config.Define('PS1', r'\\s-\\v')
39
40 # YSH: set ENV.PS1
41 # OSH: set PS1
42 env_config.InitFromEnv('PS1')
43
44 # YSH - get from ENV or __default__
45 env_config.Get('PS1')
46
47 # Custom logic for PWD
48 if not env_config.Exists('PWD'):
49 pass
50
51 More features:
52
53 - On-demand BASHPID
54 - io.thisPid() - is BASHPID
55 - io.pid() - is $$
56 - Init-once UID EUID PPID
57 - maybe this should be a separate Funcs class?
58 - io.uid() io.euid() io.ppid()
59 """
60
61 def __init__(self, mem, defaults):
62 # type: (state.Mem, Dict[str, value_t]) -> None
63
64 # mutates env_dict
65 self.mem = mem
66 self.exec_opts = mem.exec_opts
67 self.defaults = defaults
68
69 def Define(self, var_name, default_s):
70 # type: (str, str) -> None
71 """
72 """
73
74 def GetVal(self, var_name):
75 # type: (str) -> value_t
76 """
77 YSH: Look at ENV.PATH, and then __defaults__.PATH
78 OSH: Look at $PATH
79 """
80 if self.mem.exec_opts.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 if self.mem.exec_opts.env_obj(): # e.g. $[ENV.PATH]
109 self.mem.defaults[var_name] = value.Str(s)
110 else:
111 state.SetGlobalString(self.mem, var_name, s)
112
113
114class ShellFiles(object):
115
116 def __init__(self, lang, home_dir, mem, flag):
117 # type: (str, str, state.Mem, arg_types.main) -> None
118 assert lang in ('osh', 'ysh'), lang
119 self.lang = lang
120 self.home_dir = home_dir
121 self.mem = mem
122 self.flag = flag
123
124 self.init_done = False
125
126 def HistVar(self):
127 # type: () -> str
128 return 'HISTFILE' if self.lang == 'osh' else 'YSH_HISTFILE'
129
130 def DefaultHistoryFile(self):
131 # type: () -> str
132 return os_path.join(self.home_dir,
133 '.local/share/oils/%s_history' % self.lang)
134
135 def HistoryFile(self):
136 # type: () -> Optional[str]
137 assert self.init_done
138
139 # TODO: In non-strict mode we should try to cast the HISTFILE value to a
140 # string following bash's rules
141 if 0:
142 UP_val = self.mem.GetValue(self.HistVar())
143 if UP_val.tag() == value_e.Str:
144 val = cast(value.Str, UP_val)
145 return val.s
146 else:
147 # Note: if HISTFILE is an array, bash will return ${HISTFILE[0]}
148 return None
149
150 #return state.GetStringFromEnv(self.mem, self.HistVar())
151 return self.mem.env_config.Get(self.HistVar())
152
153
154def GetWorkingDir():
155 # type: () -> str
156 """Fallback for pwd and $PWD when there's no 'cd' and no inherited $PWD."""
157 try:
158 return posix.getcwd()
159 except (IOError, OSError) as e:
160 e_die("Can't determine working directory: %s" % pyutil.strerror(e))
161
162
163# This was derived from bash --norc -c 'argv "$COMP_WORDBREAKS".
164# Python overwrites this to something Python-specific in Modules/readline.c, so
165# we have to set it back!
166# Used in both core/competion.py and osh/state.py
167_READLINE_DELIMS = ' \t\n"\'><=;|&(:'
168
169
170def InitDefaultVars(mem):
171 # type: (state.Mem) -> None
172
173 # These 3 are special, can't be changed
174 state.SetGlobalString(mem, 'UID', str(posix.getuid()))
175 state.SetGlobalString(mem, 'EUID', str(posix.geteuid()))
176 state.SetGlobalString(mem, 'PPID', str(posix.getppid()))
177
178 # For getopts builtin - meant to be read, not changed
179 state.SetGlobalString(mem, 'OPTIND', '1')
180
181 # These can be changed. Could go AFTER environment, e.g. in
182 # InitVarsAfterEnv().
183
184 # Default value; user may unset it.
185 # $ echo -n "$IFS" | python -c 'import sys;print repr(sys.stdin.read())'
186 # ' \t\n'
187 state.SetGlobalString(mem, 'IFS', split.DEFAULT_IFS)
188
189 state.SetGlobalString(mem, 'HOSTNAME', libc.gethostname())
190
191 # In bash, this looks like 'linux-gnu', 'linux-musl', etc. Scripts test
192 # for 'darwin' and 'freebsd' too. They generally don't like at 'gnu' or
193 # 'musl'. We don't have that info, so just make it 'linux'.
194 state.SetGlobalString(mem, 'OSTYPE', pyos.OsType())
195
196 # When xtrace_rich is off, this is just like '+ ', the shell default
197 state.SetGlobalString(mem, 'PS4',
198 '${SHX_indent}${SHX_punct}${SHX_pid_str} ')
199
200 # bash-completion uses this. Value copied from bash. It doesn't integrate
201 # with 'readline' yet.
202 state.SetGlobalString(mem, 'COMP_WORDBREAKS', _READLINE_DELIMS)
203
204 # TODO on $HOME: bash sets it if it's a login shell and not in POSIX mode!
205 # if (login_shell == 1 && posixly_correct == 0)
206 # set_home_var ();
207
208
209def CopyVarsFromEnv(exec_opts, environ, mem):
210 # type: (optview.Exec, Dict[str, str], state.Mem) -> None
211
212 # POSIX shell behavior: env vars become exported global vars
213 if not exec_opts.no_exported():
214 # This is the way dash and bash work -- at startup, they turn everything in
215 # 'environ' variable into shell variables. Bash has an export_env
216 # variable. Dash has a loop through environ in init.c
217 for n, v in iteritems(environ):
218 mem.SetNamed(location.LName(n),
219 value.Str(v),
220 scope_e.GlobalOnly,
221 flags=state.SetExport)
222
223 # YSH behavior: env vars go in ENV dict, not exported vars. Note that
224 # ysh:upgrade can have BOTH ENV and exported vars. It's OK if they're on
225 # at the same time.
226 if exec_opts.env_obj():
227 # This is for invoking bin/ysh
228 # If you run bin/osh, then exec_opts.env_obj() will be FALSE at this point.
229 # When you write shopt --set ysh:all or ysh:upgrade, then the shopt
230 # builtin will call MaybeInitEnvDict()
231 mem.MaybeInitEnvDict(environ)
232
233
234def InitVarsAfterEnv(mem):
235 # type: (state.Mem) -> None
236
237 # If PATH SHELLOPTS PWD are not in environ, then initialize them.
238 s = mem.env_config.Get('PATH')
239 if s is None:
240 # Setting PATH to these two dirs match what zsh and mksh do. bash and
241 # dash add {,/usr/,/usr/local}/{bin,sbin}
242 mem.env_config.SetDefault('PATH', '/bin:/usr/bin')
243 #state.SetStringInEnv(mem, 'PATH', '/bin:/usr/bin')
244
245 val = mem.GetValue('SHELLOPTS')
246 if val.tag() == value_e.Undef:
247 # Divergence: bash constructs a string here too, it doesn't just read it
248 state.SetGlobalString(mem, 'SHELLOPTS', '')
249 # It's readonly, even if it's not set
250 mem.SetNamed(location.LName('SHELLOPTS'),
251 None,
252 scope_e.GlobalOnly,
253 flags=state.SetReadOnly)
254 # NOTE: bash also has BASHOPTS
255
256 val = mem.GetValue('PWD')
257 if val.tag() == value_e.Undef:
258 state.SetGlobalString(mem, 'PWD', GetWorkingDir())
259 # It's EXPORTED, even if it's not set. bash and dash both do this:
260 # env -i -- dash -c env
261 mem.SetNamed(location.LName('PWD'),
262 None,
263 scope_e.GlobalOnly,
264 flags=state.SetExport)
265
266 # Set a MUTABLE GLOBAL that's SEPARATE from $PWD. It's used by the 'pwd'
267 # builtin, and it can't be modified by users.
268 val = mem.GetValue('PWD')
269 assert val.tag() == value_e.Str, val
270 pwd = cast(value.Str, val).s
271 mem.SetPwd(pwd)
272
273
274def InitInteractive(mem, sh_files, lang):
275 # type: (state.Mem, ShellFiles, str) -> None
276 """Initialization that's only done in the interactive/headless shell."""
277
278 ps1_str = mem.env_config.Get('PS1')
279 if ps1_str is None:
280 # TODO: I don't want to export this default
281 state.SetStringInEnv(mem, 'PS1', r'\s-\v\$ ')
282 else:
283 if lang == 'ysh':
284 state.SetStringInEnv(mem, 'PS1', 'ysh ' + ps1_str)
285
286 if 0:
287 ps1_str = state.GetStringFromEnv(mem, 'PS1')
288 if ps1_str is None:
289 state.SetStringInEnv(mem, 'PS1', r'\s-\v\$ ')
290 else:
291 if lang == 'ysh':
292 state.SetStringInEnv(mem, 'PS1', 'ysh ' + ps1_str)
293
294 if 0:
295 mem.env_config.defaults['PS1'] = value.Str(r'\s-\v\$ ')
296 ps1_str = mem.env_config.Get('PS1')
297 #log('ps1 %r', ps1_str)
298 if ps1_str is not None:
299 if lang == 'ysh': # YSH prepends 'ysh ' to PS1
300 #state.SetStringInEnv(mem, 'PS1', 'ysh ' + ps1_str)
301 #mem.env_config.defaults['PS1'] = value.Str('ysh ' + ps1_str)
302 mem.env_dict['PS1'] = value.Str('ysh ' + ps1_str)
303 #log('YSH %r', ps1_str)
304
305 # TODO: use env_config
306 hist_var = sh_files.HistVar()
307 hist_val = mem.GetValue(hist_var)
308 if hist_val.tag() == value_e.Undef:
309 default_val = sh_files.DefaultHistoryFile()
310 # Note: if the directory doesn't exist, GNU readline ignores it
311 # This is like
312 # HISTFILE=foo
313 # setglobal HISTFILE = 'foo'
314 # Not like:
315 # export HISTFILE=foo
316 # setglobal ENV.HISTFILE = 'foo'
317 #
318 # Note: bash only sets this in interactive shells
319 state.SetGlobalString(mem, hist_var, default_val)
320
321 sh_files.init_done = True # sanity check before using sh_files
322
323 # Old logic:
324 if 0:
325 # PS1 is set, and it's YSH, then prepend 'ysh' to it to eliminate confusion
326 ps1_val = mem.GetValue('PS1')
327 with tagswitch(ps1_val) as case:
328 if case(value_e.Undef):
329 # Same default PS1 as bash
330 state.SetGlobalString(mem, 'PS1', r'\s-\v\$ ')
331
332 elif case(value_e.Str):
333 # Hack so we don't confuse osh and ysh, but we still respect the
334 # PS1.
335
336 # The user can disable this with
337 #
338 # func renderPrompt() {
339 # return ("${PS1@P}")
340 # }
341 if lang == 'ysh':
342 user_setting = cast(value.Str, ps1_val).s
343 state.SetGlobalString(mem, 'PS1', 'ysh ' + user_setting)
344
345
346def InitBuiltins(mem, version_str, defaults):
347 # type: (state.Mem, str, Dict[str, value_t]) -> None
348 """Initialize memory with shell defaults.
349
350 Other interpreters could have different builtin variables.
351 """
352 # TODO: REMOVE this legacy. ble.sh checks it!
353 mem.builtins['OIL_VERSION'] = value.Str(version_str)
354
355 mem.builtins['OILS_VERSION'] = value.Str(version_str)
356
357 mem.builtins['__defaults__'] = value.Dict(defaults)
358
359 # The source builtin understands '///' to mean "relative to embedded stdlib"
360 mem.builtins['LIB_OSH'] = value.Str('///osh')
361 mem.builtins['LIB_YSH'] = value.Str('///ysh')
362
363 # - C spells it NAN
364 # - JavaScript spells it NaN
365 # - Python 2 has float('nan'), while Python 3 has math.nan.
366 #
367 # - libc prints the strings 'nan' and 'inf'
368 # - Python 3 prints the strings 'nan' and 'inf'
369 # - JavaScript prints 'NaN' and 'Infinity', which is more stylized
370 mem.builtins['NAN'] = value.Float(pyutil.nan())
371 mem.builtins['INFINITY'] = value.Float(pyutil.infinity())