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

288 lines, 129 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
34 Features TODO
35
36 - On-demand BASHPID
37 - io.thisPid() - is BASHPID
38 - io.pid() - is $$
39 - Init-once UID EUID PPID
40 - maybe this should be a separate Funcs class?
41 - io.uid() io.euid() io.ppid()
42 """
43
44 def __init__(self, mem, defaults):
45 # type: (state.Mem, Dict[str, value_t]) -> None
46 self.mem = mem
47 self.exec_opts = mem.exec_opts
48 self.defaults = defaults
49
50 def GetVal(self, var_name):
51 # type: (str) -> value_t
52 """
53 YSH: Look at ENV.PATH, and then __defaults__.PATH
54 OSH: Look at $PATH
55 """
56 if self.mem.exec_opts.env_obj(): # e.g. $[ENV.PATH]
57
58 val = self.mem.env_dict.get(var_name)
59 if val is None:
60 val = self.defaults.get(var_name)
61
62 if val is None:
63 return value.Undef
64
65 #log('**ENV obj val = %s', val)
66
67 else: # e.g. $PATH
68 val = self.mem.GetValue(var_name)
69
70 return val
71
72 def Get(self, var_name):
73 # type: (str) -> Optional[str]
74 """
75 Like GetVal(), but returns a strin, or None
76 """
77 val = self.GetVal(var_name)
78 if val.tag() != value_e.Str:
79 return None
80 return cast(value.Str, val).s
81
82 def SetDefault(self, var_name, s):
83 # type: (str, str) -> None
84 """
85 OSH: Set HISTFILE var, which is read by GetVal()
86 YSH: Set __defaults__.YSH_HISTFILE, which is also read by GetVal()
87 """
88 if self.mem.exec_opts.env_obj(): # e.g. $[ENV.PATH]
89 self.mem.defaults[var_name] = value.Str(s)
90 else:
91 state.SetGlobalString(self.mem, var_name, s)
92
93
94class ShellFiles(object):
95
96 def __init__(self, lang, home_dir, mem, flag):
97 # type: (str, str, state.Mem, arg_types.main) -> None
98 assert lang in ('osh', 'ysh'), lang
99 self.lang = lang
100 self.home_dir = home_dir
101 self.mem = mem
102 self.flag = flag
103
104 self.init_done = False
105
106 def HistVar(self):
107 # type: () -> str
108 return 'HISTFILE' if self.lang == 'osh' else 'YSH_HISTFILE'
109
110 def DefaultHistoryFile(self):
111 # type: () -> str
112 return os_path.join(self.home_dir,
113 '.local/share/oils/%s_history' % self.lang)
114
115 def HistoryFile(self):
116 # type: () -> Optional[str]
117 assert self.init_done
118
119 return self.mem.env_config.Get(self.HistVar())
120
121
122def GetWorkingDir():
123 # type: () -> str
124 """Fallback for pwd and $PWD when there's no 'cd' and no inherited $PWD."""
125 try:
126 return posix.getcwd()
127 except (IOError, OSError) as e:
128 e_die("Can't determine working directory: %s" % pyutil.strerror(e))
129
130
131# This was derived from bash --norc -c 'argv "$COMP_WORDBREAKS".
132# Python overwrites this to something Python-specific in Modules/readline.c, so
133# we have to set it back!
134# Used in both core/competion.py and osh/state.py
135_READLINE_DELIMS = ' \t\n"\'><=;|&(:'
136
137
138def InitDefaultVars(mem):
139 # type: (state.Mem) -> None
140
141 # These 3 are special, can't be changed
142 state.SetGlobalString(mem, 'UID', str(posix.getuid()))
143 state.SetGlobalString(mem, 'EUID', str(posix.geteuid()))
144 state.SetGlobalString(mem, 'PPID', str(posix.getppid()))
145
146 # For getopts builtin - meant to be read, not changed
147 state.SetGlobalString(mem, 'OPTIND', '1')
148
149 # These can be changed. Could go AFTER environment, e.g. in
150 # InitVarsAfterEnv().
151
152 # Default value; user may unset it.
153 # $ echo -n "$IFS" | python -c 'import sys;print repr(sys.stdin.read())'
154 # ' \t\n'
155 state.SetGlobalString(mem, 'IFS', split.DEFAULT_IFS)
156
157 state.SetGlobalString(mem, 'HOSTNAME', libc.gethostname())
158
159 # In bash, this looks like 'linux-gnu', 'linux-musl', etc. Scripts test
160 # for 'darwin' and 'freebsd' too. They generally don't like at 'gnu' or
161 # 'musl'. We don't have that info, so just make it 'linux'.
162 state.SetGlobalString(mem, 'OSTYPE', pyos.OsType())
163
164 # bash-completion uses this. Value copied from bash. It doesn't integrate
165 # with 'readline' yet.
166 state.SetGlobalString(mem, 'COMP_WORDBREAKS', _READLINE_DELIMS)
167
168 # TODO on $HOME: bash sets it if it's a login shell and not in POSIX mode!
169 # if (login_shell == 1 && posixly_correct == 0)
170 # set_home_var ();
171
172
173def CopyVarsFromEnv(exec_opts, environ, mem):
174 # type: (optview.Exec, Dict[str, str], state.Mem) -> None
175
176 # POSIX shell behavior: env vars become exported global vars
177 if not exec_opts.no_exported():
178 # This is the way dash and bash work -- at startup, they turn everything in
179 # 'environ' variable into shell variables. Bash has an export_env
180 # variable. Dash has a loop through environ in init.c
181 for n, v in iteritems(environ):
182 mem.SetNamed(location.LName(n),
183 value.Str(v),
184 scope_e.GlobalOnly,
185 flags=state.SetExport)
186
187 # YSH behavior: env vars go in ENV dict, not exported vars. Note that
188 # ysh:upgrade can have BOTH ENV and exported vars. It's OK if they're on
189 # at the same time.
190 if exec_opts.env_obj():
191 # This is for invoking bin/ysh
192 # If you run bin/osh, then exec_opts.env_obj() will be FALSE at this point.
193 # When you write shopt --set ysh:all or ysh:upgrade, then the shopt
194 # builtin will call MaybeInitEnvDict()
195 mem.MaybeInitEnvDict(environ)
196
197
198def InitVarsAfterEnv(mem):
199 # type: (state.Mem) -> None
200
201 s = mem.env_config.Get('PS4')
202 if s is None:
203 # When xtrace_rich is off, this is just like '+ ', the shell default
204 mem.env_config.SetDefault('PS4',
205 '${SHX_indent}${SHX_punct}${SHX_pid_str} ')
206
207 # If PATH SHELLOPTS PWD are not in environ, then initialize them.
208 s = mem.env_config.Get('PATH')
209 if s is None:
210 # Setting PATH to these two dirs match what zsh and mksh do. bash and
211 # dash add {,/usr/,/usr/local}/{bin,sbin}
212 mem.env_config.SetDefault('PATH', '/bin:/usr/bin')
213
214 val = mem.GetValue('SHELLOPTS')
215 if val.tag() == value_e.Undef:
216 # Divergence: bash constructs a string here too, it doesn't just read it
217 state.SetGlobalString(mem, 'SHELLOPTS', '')
218 # It's readonly, even if it's not set
219 mem.SetNamed(location.LName('SHELLOPTS'),
220 None,
221 scope_e.GlobalOnly,
222 flags=state.SetReadOnly)
223 # NOTE: bash also has BASHOPTS
224
225 val = mem.GetValue('PWD')
226 if val.tag() == value_e.Undef:
227 state.SetGlobalString(mem, 'PWD', GetWorkingDir())
228 # It's EXPORTED, even if it's not set. bash and dash both do this:
229 # env -i -- dash -c env
230 mem.SetNamed(location.LName('PWD'),
231 None,
232 scope_e.GlobalOnly,
233 flags=state.SetExport)
234
235 # Set a MUTABLE GLOBAL that's SEPARATE from $PWD. It's used by the 'pwd'
236 # builtin, and it can't be modified by users.
237 val = mem.GetValue('PWD')
238 assert val.tag() == value_e.Str, val
239 pwd = cast(value.Str, val).s
240 mem.SetPwd(pwd)
241
242
243def InitInteractive(mem, sh_files, lang):
244 # type: (state.Mem, ShellFiles, str) -> None
245 """Initialization that's only done in the interactive/headless shell."""
246
247 ps1_str = mem.env_config.Get('PS1')
248 if ps1_str is None:
249 mem.env_config.SetDefault('PS1', r'\s-\v\$ ')
250 else:
251 if lang == 'ysh':
252 # If this is bin/ysh, and we got a plain PS1, then prepend 'ysh ' to PS1
253 mem.env_dict['PS1'] = value.Str('ysh ' + ps1_str)
254
255 hist_var = sh_files.HistVar()
256 hist_str = mem.env_config.Get(hist_var)
257 if hist_str is None:
258 mem.env_config.SetDefault(hist_var, sh_files.DefaultHistoryFile())
259
260 sh_files.init_done = True # sanity check before using sh_files
261
262
263def InitBuiltins(mem, version_str, defaults):
264 # type: (state.Mem, str, Dict[str, value_t]) -> None
265 """Initialize memory with shell defaults.
266
267 Other interpreters could have different builtin variables.
268 """
269 # TODO: REMOVE this legacy. ble.sh checks it!
270 mem.builtins['OIL_VERSION'] = value.Str(version_str)
271
272 mem.builtins['OILS_VERSION'] = value.Str(version_str)
273
274 mem.builtins['__defaults__'] = value.Dict(defaults)
275
276 # The source builtin understands '///' to mean "relative to embedded stdlib"
277 mem.builtins['LIB_OSH'] = value.Str('///osh')
278 mem.builtins['LIB_YSH'] = value.Str('///ysh')
279
280 # - C spells it NAN
281 # - JavaScript spells it NaN
282 # - Python 2 has float('nan'), while Python 3 has math.nan.
283 #
284 # - libc prints the strings 'nan' and 'inf'
285 # - Python 3 prints the strings 'nan' and 'inf'
286 # - JavaScript prints 'NaN' and 'Infinity', which is more stylized
287 mem.builtins['NAN'] = value.Float(pyutil.nan())
288 mem.builtins['INFINITY'] = value.Float(pyutil.infinity())