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

314 lines, 140 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 two dirs match what zsh and mksh do. bash and
216 # dash add {,/usr/,/usr/local}/{bin,sbin}
217 mem.env_config.SetDefault('PATH', '/bin:/usr/bin')
218
219 if mem.exec_opts.no_init_globals():
220 # YSH initialization
221 mem.SetPwd(GetWorkingDir())
222
223 # TODO: YSH can use cross-process tracing with SHELLOPTS, BASHOPTS, and
224 # OILS_OPTS?
225 # Or at least the xtrace stuff should be in OILS_OPTS. Bash has a
226 # quirk where these env vars turn options ON, but they don't turn
227 # options OFF. So it is perhaps not a great mechanism.
228 else:
229 # OSH initialization
230 shellopts = mem.GetValue('SHELLOPTS')
231 UP_shellopts = shellopts
232 with tagswitch(shellopts) as case:
233 if case(value_e.Str):
234 shellopts = cast(value.Str, UP_shellopts)
235 mutable_opts.InitFromEnv(shellopts.s)
236 elif case(value_e.Undef):
237 # If it's not in the environment, construct the string
238 state.SetGlobalString(mem, 'SHELLOPTS',
239 mutable_opts.ShelloptsString())
240 else:
241 raise AssertionError()
242
243 # Mark it readonly, like bash
244 mem.SetNamed(location.LName('SHELLOPTS'),
245 None,
246 scope_e.GlobalOnly,
247 flags=state.SetReadOnly)
248
249 # NOTE: bash also has BASHOPTS
250
251 val = mem.GetValue('PWD')
252 if val.tag() == value_e.Undef:
253 state.SetGlobalString(mem, 'PWD', GetWorkingDir())
254 # It's EXPORTED, even if it's not set. bash and dash both do this:
255 # env -i -- dash -c env
256 mem.SetNamed(location.LName('PWD'),
257 None,
258 scope_e.GlobalOnly,
259 flags=state.SetExport)
260
261 # Set a MUTABLE GLOBAL that's SEPARATE from $PWD. It's used by the 'pwd'
262 # builtin, and it can't be modified by users.
263 val = mem.GetValue('PWD')
264 assert val.tag() == value_e.Str, val
265 pwd = cast(value.Str, val).s
266 mem.SetPwd(pwd)
267
268
269def InitInteractive(mem, sh_files, lang):
270 # type: (state.Mem, ShellFiles, str) -> None
271 """Initialization that's only done in the interactive/headless shell."""
272
273 ps1_str = mem.env_config.Get('PS1')
274 if ps1_str is None:
275 mem.env_config.SetDefault('PS1', r'\s-\v\$ ')
276 else:
277 if lang == 'ysh':
278 # If this is bin/ysh, and we got a plain PS1, then prepend 'ysh ' to PS1
279 mem.env_dict['PS1'] = value.Str('ysh ' + ps1_str)
280
281 hist_var = sh_files.HistVar()
282 hist_str = mem.env_config.Get(hist_var)
283 if hist_str is None:
284 mem.env_config.SetDefault(hist_var, sh_files.DefaultHistoryFile())
285
286 sh_files.init_done = True # sanity check before using sh_files
287
288
289def InitBuiltins(mem, version_str, defaults):
290 # type: (state.Mem, str, Dict[str, value_t]) -> None
291 """Initialize memory with shell defaults.
292
293 Other interpreters could have different builtin variables.
294 """
295 # TODO: REMOVE this legacy. ble.sh checks it!
296 mem.builtins['OIL_VERSION'] = value.Str(version_str)
297
298 mem.builtins['OILS_VERSION'] = value.Str(version_str)
299
300 mem.builtins['__defaults__'] = value.Dict(defaults)
301
302 # The source builtin understands '///' to mean "relative to embedded stdlib"
303 mem.builtins['LIB_OSH'] = value.Str('///osh')
304 mem.builtins['LIB_YSH'] = value.Str('///ysh')
305
306 # - C spells it NAN
307 # - JavaScript spells it NaN
308 # - Python 2 has float('nan'), while Python 3 has math.nan.
309 #
310 # - libc prints the strings 'nan' and 'inf'
311 # - Python 3 prints the strings 'nan' and 'inf'
312 # - JavaScript prints 'NaN' and 'Infinity', which is more stylized
313 mem.builtins['NAN'] = value.Float(pyutil.nan())
314 mem.builtins['INFINITY'] = value.Float(pyutil.infinity())