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

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