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

293 lines, 130 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, 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 """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 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 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 working directory: %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):
140 # type: (state.Mem) -> None
141
142 # These 3 are special, can't be changed
143 state.SetGlobalString(mem, 'UID', str(posix.getuid()))
144 state.SetGlobalString(mem, 'EUID', str(posix.geteuid()))
145 state.SetGlobalString(mem, 'PPID', str(posix.getppid()))
146
147 # For getopts builtin - meant to be read, not changed
148 state.SetGlobalString(mem, 'OPTIND', '1')
149
150 # These can be changed. Could go AFTER environment, e.g. in
151 # InitVarsAfterEnv().
152
153 # Default value; user may unset it.
154 # $ echo -n "$IFS" | python -c 'import sys;print repr(sys.stdin.read())'
155 # ' \t\n'
156 state.SetGlobalString(mem, 'IFS', split.DEFAULT_IFS)
157
158 state.SetGlobalString(mem, 'HOSTNAME', libc.gethostname())
159
160 # In bash, this looks like 'linux-gnu', 'linux-musl', etc. Scripts test
161 # for 'darwin' and 'freebsd' too. They generally don't like at 'gnu' or
162 # 'musl'. We don't have that info, so just make it 'linux'.
163 state.SetGlobalString(mem, 'OSTYPE', pyos.OsType())
164
165 # When xtrace_rich is off, this is just like '+ ', the shell default
166 state.SetGlobalString(mem, 'PS4',
167 '${SHX_indent}${SHX_punct}${SHX_pid_str} ')
168
169 # bash-completion uses this. Value copied from bash. It doesn't integrate
170 # with 'readline' yet.
171 state.SetGlobalString(mem, 'COMP_WORDBREAKS', _READLINE_DELIMS)
172
173 # TODO on $HOME: bash sets it if it's a login shell and not in POSIX mode!
174 # if (login_shell == 1 && posixly_correct == 0)
175 # set_home_var ();
176
177
178def CopyVarsFromEnv(exec_opts, environ, mem):
179 # type: (optview.Exec, Dict[str, str], state.Mem) -> None
180
181 # POSIX shell behavior: env vars become exported global vars
182 if not exec_opts.no_exported():
183 # This is the way dash and bash work -- at startup, they turn everything in
184 # 'environ' variable into shell variables. Bash has an export_env
185 # variable. Dash has a loop through environ in init.c
186 for n, v in iteritems(environ):
187 mem.SetNamed(location.LName(n),
188 value.Str(v),
189 scope_e.GlobalOnly,
190 flags=state.SetExport)
191
192 # YSH behavior: env vars go in ENV dict, not exported vars. Note that
193 # ysh:upgrade can have BOTH ENV and exported vars. It's OK if they're on
194 # at the same time.
195 if exec_opts.env_obj():
196 # This is for invoking bin/ysh
197 # If you run bin/osh, then exec_opts.env_obj() will be FALSE at this point.
198 # When you write shopt --set ysh:all or ysh:upgrade, then the shopt
199 # builtin will call MaybeInitEnvDict()
200 mem.MaybeInitEnvDict(environ)
201
202
203def InitVarsAfterEnv(mem):
204 # type: (state.Mem) -> None
205
206 # If PATH SHELLOPTS PWD are not in environ, then initialize them.
207 s = mem.env_config.Get('PATH')
208 if s is None:
209 # Setting PATH to these two dirs match what zsh and mksh do. bash and
210 # dash add {,/usr/,/usr/local}/{bin,sbin}
211 mem.env_config.SetDefault('PATH', '/bin:/usr/bin')
212
213 if not mem.exec_opts.no_init_globals():
214 # OSH initialization
215 val = mem.GetValue('SHELLOPTS')
216 if val.tag() == value_e.Undef:
217 # Divergence: bash constructs a string here too, it doesn't just read it
218 state.SetGlobalString(mem, 'SHELLOPTS', '')
219 # It's readonly, even if it's not set
220 mem.SetNamed(location.LName('SHELLOPTS'),
221 None,
222 scope_e.GlobalOnly,
223 flags=state.SetReadOnly)
224 # NOTE: bash also has BASHOPTS
225
226 val = mem.GetValue('PWD')
227 if val.tag() == value_e.Undef:
228 state.SetGlobalString(mem, 'PWD', GetWorkingDir())
229 # It's EXPORTED, even if it's not set. bash and dash both do this:
230 # env -i -- dash -c env
231 mem.SetNamed(location.LName('PWD'),
232 None,
233 scope_e.GlobalOnly,
234 flags=state.SetExport)
235
236 # Set a MUTABLE GLOBAL that's SEPARATE from $PWD. It's used by the 'pwd'
237 # builtin, and it can't be modified by users.
238 val = mem.GetValue('PWD')
239 assert val.tag() == value_e.Str, val
240 pwd = cast(value.Str, val).s
241 mem.SetPwd(pwd)
242
243 else:
244 # YSH initialization
245 mem.SetPwd(GetWorkingDir())
246
247
248def InitInteractive(mem, sh_files, lang):
249 # type: (state.Mem, ShellFiles, str) -> None
250 """Initialization that's only done in the interactive/headless shell."""
251
252 ps1_str = mem.env_config.Get('PS1')
253 if ps1_str is None:
254 mem.env_config.SetDefault('PS1', r'\s-\v\$ ')
255 else:
256 if lang == 'ysh':
257 # If this is bin/ysh, and we got a plain PS1, then prepend 'ysh ' to PS1
258 mem.env_dict['PS1'] = value.Str('ysh ' + ps1_str)
259
260 hist_var = sh_files.HistVar()
261 hist_str = mem.env_config.Get(hist_var)
262 if hist_str is None:
263 mem.env_config.SetDefault(hist_var, sh_files.DefaultHistoryFile())
264
265 sh_files.init_done = True # sanity check before using sh_files
266
267
268def InitBuiltins(mem, version_str, defaults):
269 # type: (state.Mem, str, Dict[str, value_t]) -> None
270 """Initialize memory with shell defaults.
271
272 Other interpreters could have different builtin variables.
273 """
274 # TODO: REMOVE this legacy. ble.sh checks it!
275 mem.builtins['OIL_VERSION'] = value.Str(version_str)
276
277 mem.builtins['OILS_VERSION'] = value.Str(version_str)
278
279 mem.builtins['__defaults__'] = value.Dict(defaults)
280
281 # The source builtin understands '///' to mean "relative to embedded stdlib"
282 mem.builtins['LIB_OSH'] = value.Str('///osh')
283 mem.builtins['LIB_YSH'] = value.Str('///ysh')
284
285 # - C spells it NAN
286 # - JavaScript spells it NaN
287 # - Python 2 has float('nan'), while Python 3 has math.nan.
288 #
289 # - libc prints the strings 'nan' and 'inf'
290 # - Python 3 prints the strings 'nan' and 'inf'
291 # - JavaScript prints 'NaN' and 'Infinity', which is more stylized
292 mem.builtins['NAN'] = value.Float(pyutil.nan())
293 mem.builtins['INFINITY'] = value.Float(pyutil.infinity())