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

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