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

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