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

320 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 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 our_pwd = None # type: Optional[str]
252 val = mem.GetValue('PWD')
253 if val.tag() == value_e.Str:
254 env_pwd = cast(value.Str, val).s
255 # POSIX rule: PWD is inherited if it's an absolute path that corresponds to '.'
256 if env_pwd.startswith('/') and pyos.IsSameFile(env_pwd, '.'):
257 our_pwd = env_pwd
258
259 # POSIX: Otherwise, recalculate it
260 if our_pwd is None:
261 our_pwd = GetWorkingDir()
262
263 # It's EXPORTED, even if it's not set. bash and dash both do this:
264 # env -i -- dash -c env
265 mem.SetNamed(location.LName('PWD'),
266 value.Str(our_pwd),
267 scope_e.GlobalOnly,
268 flags=state.SetExport)
269
270 # Set a MUTABLE GLOBAL that's SEPARATE from $PWD. It's used by the 'pwd'
271 # builtin, and it can't be modified by users.
272 mem.SetPwd(our_pwd)
273
274
275def InitInteractive(mem, sh_files, lang):
276 # type: (state.Mem, ShellFiles, str) -> None
277 """Initialization that's only done in the interactive/headless shell."""
278
279 ps1_str = mem.env_config.Get('PS1')
280 if ps1_str is None:
281 mem.env_config.SetDefault('PS1', r'\s-\v\$ ')
282 else:
283 if lang == 'ysh':
284 # If this is bin/ysh, and we got a plain PS1, then prepend 'ysh ' to PS1
285 mem.env_dict['PS1'] = value.Str('ysh ' + ps1_str)
286
287 hist_var = sh_files.HistVar()
288 hist_str = mem.env_config.Get(hist_var)
289 if hist_str is None:
290 mem.env_config.SetDefault(hist_var, sh_files.DefaultHistoryFile())
291
292 sh_files.init_done = True # sanity check before using sh_files
293
294
295def InitBuiltins(mem, version_str, defaults):
296 # type: (state.Mem, str, Dict[str, value_t]) -> None
297 """Initialize memory with shell defaults.
298
299 Other interpreters could have different builtin variables.
300 """
301 # TODO: REMOVE this legacy. ble.sh checks it!
302 mem.builtins['OIL_VERSION'] = value.Str(version_str)
303
304 mem.builtins['OILS_VERSION'] = value.Str(version_str)
305
306 mem.builtins['__defaults__'] = value.Dict(defaults)
307
308 # The source builtin understands '///' to mean "relative to embedded stdlib"
309 mem.builtins['LIB_OSH'] = value.Str('///osh')
310 mem.builtins['LIB_YSH'] = value.Str('///ysh')
311
312 # - C spells it NAN
313 # - JavaScript spells it NaN
314 # - Python 2 has float('nan'), while Python 3 has math.nan.
315 #
316 # - libc prints the strings 'nan' and 'inf'
317 # - Python 3 prints the strings 'nan' and 'inf'
318 # - JavaScript prints 'NaN' and 'Infinity', which is more stylized
319 mem.builtins['NAN'] = value.Float(pyutil.nan())
320 mem.builtins['INFINITY'] = value.Float(pyutil.infinity())