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

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