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
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 if 0:
135 UP_val = self.mem.GetValue(self.HistVar())
136 if UP_val.tag() == value_e.Str:
137 val = cast(value.Str, UP_val)
138 return val.s
139 else:
140 # Note: if HISTFILE is an array, bash will return ${HISTFILE[0]}
141 return None
142
143 #return state.GetStringFromEnv(self.mem, self.HistVar())
144 return self.mem.env_config.Get(self.HistVar())
145
146
147def GetWorkingDir():
148 # type: () -> str
149 """Fallback for pwd and $PWD when there's no 'cd' and no inherited $PWD."""
150 try:
151 return posix.getcwd()
152 except (IOError, OSError) as e:
153 e_die("Can't determine working directory: %s" % pyutil.strerror(e))
154
155
156# This was derived from bash --norc -c 'argv "$COMP_WORDBREAKS".
157# Python overwrites this to something Python-specific in Modules/readline.c, so
158# we have to set it back!
159# Used in both core/competion.py and osh/state.py
160_READLINE_DELIMS = ' \t\n"\'><=;|&(:'
161
162
163def InitDefaultVars(mem):
164 # type: (state.Mem) -> None
165
166 # These 3 are special, can't be changed
167 state.SetGlobalString(mem, 'UID', str(posix.getuid()))
168 state.SetGlobalString(mem, 'EUID', str(posix.geteuid()))
169 state.SetGlobalString(mem, 'PPID', str(posix.getppid()))
170
171 # For getopts builtin - meant to be read, not changed
172 state.SetGlobalString(mem, 'OPTIND', '1')
173
174 # These can be changed. Could go AFTER environment, e.g. in
175 # InitVarsAfterEnv().
176
177 # Default value; user may unset it.
178 # $ echo -n "$IFS" | python -c 'import sys;print repr(sys.stdin.read())'
179 # ' \t\n'
180 state.SetGlobalString(mem, 'IFS', split.DEFAULT_IFS)
181
182 state.SetGlobalString(mem, 'HOSTNAME', libc.gethostname())
183
184 # In bash, this looks like 'linux-gnu', 'linux-musl', etc. Scripts test
185 # for 'darwin' and 'freebsd' too. They generally don't like at 'gnu' or
186 # 'musl'. We don't have that info, so just make it 'linux'.
187 state.SetGlobalString(mem, 'OSTYPE', pyos.OsType())
188
189 # When xtrace_rich is off, this is just like '+ ', the shell default
190 state.SetGlobalString(mem, 'PS4',
191 '${SHX_indent}${SHX_punct}${SHX_pid_str} ')
192
193 # bash-completion uses this. Value copied from bash. It doesn't integrate
194 # with 'readline' yet.
195 state.SetGlobalString(mem, 'COMP_WORDBREAKS', _READLINE_DELIMS)
196
197 # TODO on $HOME: bash sets it if it's a login shell and not in POSIX mode!
198 # if (login_shell == 1 && posixly_correct == 0)
199 # set_home_var ();
200
201
202def CopyVarsFromEnv(exec_opts, environ, mem):
203 # type: (optview.Exec, Dict[str, str], state.Mem) -> None
204
205 # POSIX shell behavior: env vars become exported global vars
206 if not exec_opts.no_exported():
207 # This is the way dash and bash work -- at startup, they turn everything in
208 # 'environ' variable into shell variables. Bash has an export_env
209 # variable. Dash has a loop through environ in init.c
210 for n, v in iteritems(environ):
211 mem.SetNamed(location.LName(n),
212 value.Str(v),
213 scope_e.GlobalOnly,
214 flags=state.SetExport)
215
216 # YSH behavior: env vars go in ENV dict, not exported vars. Note that
217 # ysh:upgrade can have BOTH ENV and exported vars. It's OK if they're on
218 # at the same time.
219 if exec_opts.env_obj():
220 # This is for invoking bin/ysh
221 # If you run bin/osh, then exec_opts.env_obj() will be FALSE at this point.
222 # When you write shopt --set ysh:all or ysh:upgrade, then the shopt
223 # builtin will call MaybeInitEnvDict()
224 mem.MaybeInitEnvDict(environ)
225
226
227def InitVarsAfterEnv(mem):
228 # type: (state.Mem) -> None
229
230 # If PATH SHELLOPTS PWD are not in environ, then initialize them.
231 if 0:
232 s = mem.env_config.Get('PATH')
233 if s is None:
234 # Setting PATH to these two dirs match what zsh and mksh do. bash and
235 # dash add {,/usr/,/usr/local}/{bin,sbin}
236 state.SetStringInEnv(mem, 'PATH', '/bin:/usr/bin')
237
238 if 1:
239 val = mem.GetValue('PATH')
240 if val.tag() == value_e.Undef:
241 # Setting PATH to these two dirs match what zsh and mksh do. bash and
242 # dash add {,/usr/,/usr/local}/{bin,sbin}
243 state.SetGlobalString(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())