OILS / builtin / dirs_osh.py View on Github | oils.pub

364 lines, 230 significant
1from __future__ import print_function
2
3from _devbuild.gen import arg_types
4from _devbuild.gen.runtime_asdl import cmd_value
5from core import error
6from core.error import e_usage
7from core import pyos
8from core import state
9from display import ui
10from core import vm
11from frontend import flag_util
12from frontend import typed_args
13from mycpp.mylib import log
14from pylib import os_path
15from pylib import path_stat
16from core import value
17from core.value import value_e
18from typing import cast
19
20import libc
21import posix_ as posix
22
23from typing import List, Optional, Any, TYPE_CHECKING
24if TYPE_CHECKING:
25 from osh.cmd_eval import CommandEvaluator
26
27_ = log
28
29
30class DirStack(object):
31 """For pushd/popd/dirs."""
32
33 def __init__(self):
34 # type: () -> None
35 self.stack = [] # type: List[str]
36 self.Reset() # Invariant: it always has at least ONE entry.
37
38 def Reset(self):
39 # type: () -> None
40 """ For dirs -c """
41 del self.stack[:]
42 self.stack.append(posix.getcwd())
43
44 def Replace(self, d):
45 # type: (str) -> None
46 """ For cd / """
47 self.stack[-1] = d
48
49 def Push(self, entry):
50 # type: (str) -> None
51 self.stack.append(entry)
52
53 def Pop(self):
54 # type: () -> Optional[str]
55 if len(self.stack) <= 1:
56 return None
57 self.stack.pop() # remove last
58 return self.stack[-1] # return second to last
59
60 def Iter(self):
61 # type: () -> List[str]
62 """Iterate in reverse order."""
63 # mycpp REWRITE:
64 #return reversed(self.stack)
65 ret = [] # type: List[str]
66 ret.extend(self.stack)
67 ret.reverse()
68 return ret
69
70
71class ctx_CdBlock(object):
72
73 def __init__(self, dir_stack, dest_dir, mem, errfmt, out_errs):
74 # type: (DirStack, str, state.Mem, ui.ErrorFormatter, List[bool]) -> None
75 dir_stack.Push(dest_dir)
76
77 self.dir_stack = dir_stack
78 self.mem = mem
79 self.errfmt = errfmt
80 self.out_errs = out_errs
81
82 def __enter__(self):
83 # type: () -> None
84 pass
85
86 def __exit__(self, type, value, traceback):
87 # type: (Any, Any, Any) -> None
88 _PopDirStack('cd', self.mem, self.dir_stack, self.errfmt,
89 self.out_errs)
90
91
92class Cd(vm._Builtin):
93
94 def __init__(self, mem, dir_stack, cmd_ev, errfmt):
95 # type: (state.Mem, DirStack, CommandEvaluator, ui.ErrorFormatter) -> None
96 self.mem = mem
97 self.dir_stack = dir_stack
98 self.cmd_ev = cmd_ev # To run blocks
99 self.errfmt = errfmt
100
101 def Run(self, cmd_val):
102 # type: (cmd_value.Argv) -> int
103 attrs, arg_r = flag_util.ParseCmdVal('cd',
104 cmd_val,
105 accept_typed_args=True)
106 arg = arg_types.cd(attrs.attrs)
107
108 # If a block is passed, we do additional syntax checks
109 cmd_frag = typed_args.OptionalBlockAsFrag(cmd_val)
110
111 dest_dir, arg_loc = arg_r.Peek2()
112 if dest_dir is None:
113 if cmd_frag:
114 raise error.Usage(
115 'requires an argument when a block is passed',
116 cmd_val.arg_locs[0])
117 else:
118 dest_dir = self.mem.env_config.Get('HOME')
119 if dest_dir is None:
120 self.errfmt.Print_(
121 "cd got no argument, and $HOME isn't set")
122 return 1
123
124 # At most 1 arg is accepted
125 arg_r.Next()
126 if self.mem.exec_opts.strict_arg_parse():
127 arg_r.Done()
128
129 # shopt -s cdable_vars allows you to type cd my_dir_var instead of cd $my_dir_var
130 if self.mem.exec_opts.cdable_vars() and dest_dir != '-':
131 if not path_stat.isdir(dest_dir):
132 val = self.mem.GetValue(dest_dir)
133 if val and val.tag() == value_e.Str:
134 val_str = cast(value.Str, val)
135 dest_dir = val_str.s
136 # Bash echoes the resolved path to stdout
137 self.errfmt.Print_(dest_dir)
138
139 if dest_dir == '-':
140 # Note: $OLDPWD isn't an env var; it's a global
141 try:
142 dest_dir = state.GetString(self.mem, 'OLDPWD')
143 print(dest_dir) # Shells print the directory
144 except error.Runtime as e:
145 self.errfmt.Print_(e.UserErrorString())
146 return 1
147
148 # Save a copy
149 old_pwd = self.mem.pwd
150
151 # Calculate absolute path, e.g. for 'cd ..'
152 # We chdir() to it, then set PWD to it.
153 # We can't call posix.getcwd() because it can raise OSError if the
154 # directory was removed (ENOENT)
155 abspath = os_path.join(old_pwd, dest_dir)
156 if arg.P:
157 # -P means resolve symbolic links, then process '..'
158 real_dest_dir = libc.realpath(abspath)
159 else:
160 # -L means process '..' first. This just does string manipulation.
161 # (But realpath afterward isn't correct?)
162 real_dest_dir = os_path.normpath(abspath)
163
164 #log('real_dest_dir %r', real_dest_dir)
165 err_num = pyos.Chdir(real_dest_dir)
166 if err_num != 0:
167 self.errfmt.Print_("cd %r: %s" %
168 (real_dest_dir, posix.strerror(err_num)),
169 blame_loc=arg_loc)
170 return 1
171
172 state.ExportGlobalString(self.mem, 'PWD', real_dest_dir)
173
174 # WEIRD: We need a copy that is NOT PWD, because the user could mutate
175 # PWD. Other shells use global variables.
176 self.mem.SetPwd(real_dest_dir)
177
178 if cmd_frag:
179 out_errs = [] # type: List[bool]
180 with ctx_CdBlock(self.dir_stack, real_dest_dir, self.mem,
181 self.errfmt, out_errs):
182 unused = self.cmd_ev.EvalCommandFrag(cmd_frag)
183 if len(out_errs):
184 return 1
185
186 else: # No block
187 state.ExportGlobalString(self.mem, 'OLDPWD', old_pwd)
188 self.dir_stack.Replace(real_dest_dir) # for pushd/popd/dirs
189
190 return 0
191
192
193WITH_LINE_NUMBERS = 1
194WITHOUT_LINE_NUMBERS = 2
195SINGLE_LINE = 3
196
197
198def _PrintDirStack(dir_stack, style, home_dir):
199 # type: (DirStack, int, Optional[str]) -> None
200 """ Helper for 'dirs' builtin """
201
202 if style == WITH_LINE_NUMBERS:
203 for i, entry in enumerate(dir_stack.Iter()):
204 print('%2d %s' % (i, ui.PrettyDir(entry, home_dir)))
205
206 elif style == WITHOUT_LINE_NUMBERS:
207 for entry in dir_stack.Iter():
208 print(ui.PrettyDir(entry, home_dir))
209
210 elif style == SINGLE_LINE:
211 parts = [ui.PrettyDir(entry, home_dir) for entry in dir_stack.Iter()]
212 s = ' '.join(parts)
213 print(s)
214
215
216class Pushd(vm._Builtin):
217
218 def __init__(self, mem, dir_stack, errfmt):
219 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
220 self.mem = mem
221 self.dir_stack = dir_stack
222 self.errfmt = errfmt
223
224 def Run(self, cmd_val):
225 # type: (cmd_value.Argv) -> int
226 _, arg_r = flag_util.ParseCmdVal('pushd', cmd_val)
227
228 dir_arg, dir_arg_loc = arg_r.Peek2()
229 if dir_arg is None:
230 # TODO: It's suppose to try another dir before doing this?
231 self.errfmt.Print_('pushd: no other directory')
232 # bash oddly returns 1, not 2
233 return 1
234
235 arg_r.Next()
236 arg_r.Done()
237
238 # TODO: 'cd' uses normpath? Is that inconsistent?
239 dest_dir = os_path.abspath(dir_arg)
240 err_num = pyos.Chdir(dest_dir)
241 if err_num != 0:
242 self.errfmt.Print_("pushd: %r: %s" %
243 (dest_dir, posix.strerror(err_num)),
244 blame_loc=dir_arg_loc)
245 return 1
246
247 self.dir_stack.Push(dest_dir)
248 _PrintDirStack(self.dir_stack, SINGLE_LINE,
249 state.MaybeString(self.mem, 'HOME'))
250 state.ExportGlobalString(self.mem, 'PWD', dest_dir)
251 self.mem.SetPwd(dest_dir)
252 return 0
253
254
255def _PopDirStack(label, mem, dir_stack, errfmt, out_errs):
256 # type: (str, state.Mem, DirStack, ui.ErrorFormatter, List[bool]) -> bool
257 """ Helper for popd and cd { ... } """
258 dest_dir = dir_stack.Pop()
259 if dest_dir is None:
260 errfmt.Print_('%s: directory stack is empty' % label)
261 out_errs.append(True) # "return" to caller
262 return False
263
264 err_num = pyos.Chdir(dest_dir)
265 if err_num != 0:
266 # Happens if a directory is deleted in pushing and popping
267 errfmt.Print_('%s: %r: %s' %
268 (label, dest_dir, posix.strerror(err_num)))
269 out_errs.append(True) # "return" to caller
270 return False
271
272 state.SetGlobalString(mem, 'PWD', dest_dir)
273 mem.SetPwd(dest_dir)
274 return True
275
276
277class Popd(vm._Builtin):
278
279 def __init__(self, mem, dir_stack, errfmt):
280 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
281 self.mem = mem
282 self.dir_stack = dir_stack
283 self.errfmt = errfmt
284
285 def Run(self, cmd_val):
286 # type: (cmd_value.Argv) -> int
287 _, arg_r = flag_util.ParseCmdVal('pushd', cmd_val)
288
289 extra, extra_loc = arg_r.Peek2()
290 if extra is not None:
291 e_usage('got extra argument', extra_loc)
292
293 out_errs = [] # type: List[bool]
294 _PopDirStack('popd', self.mem, self.dir_stack, self.errfmt, out_errs)
295 if len(out_errs):
296 return 1 # error
297
298 _PrintDirStack(self.dir_stack, SINGLE_LINE,
299 state.MaybeString(self.mem, ('HOME')))
300 return 0
301
302
303class Dirs(vm._Builtin):
304
305 def __init__(self, mem, dir_stack, errfmt):
306 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
307 self.mem = mem
308 self.dir_stack = dir_stack
309 self.errfmt = errfmt
310
311 def Run(self, cmd_val):
312 # type: (cmd_value.Argv) -> int
313 attrs, arg_r = flag_util.ParseCmdVal('dirs', cmd_val)
314 arg = arg_types.dirs(attrs.attrs)
315
316 home_dir = state.MaybeString(self.mem, 'HOME')
317 style = SINGLE_LINE
318 arg_r.Done()
319
320 # Following bash order of flag priority
321 if arg.l:
322 home_dir = None # disable pretty ~
323 if arg.c:
324 self.dir_stack.Reset()
325 return 0
326 elif arg.v:
327 style = WITH_LINE_NUMBERS
328 elif arg.p:
329 style = WITHOUT_LINE_NUMBERS
330
331 _PrintDirStack(self.dir_stack, style, home_dir)
332 return 0
333
334
335class Pwd(vm._Builtin):
336 """
337 NOTE: pwd doesn't just call getcwd(), which returns a "physical" dir (not a
338 symlink).
339 """
340
341 def __init__(self, mem, errfmt):
342 # type: (state.Mem, ui.ErrorFormatter) -> None
343 self.mem = mem
344 self.errfmt = errfmt
345
346 def Run(self, cmd_val):
347 # type: (cmd_value.Argv) -> int
348 attrs, arg_r = flag_util.ParseCmdVal('pwd', cmd_val)
349 arg = arg_types.pwd(attrs.attrs)
350
351 if self.mem.exec_opts.strict_arg_parse():
352 arg_r.Done()
353
354 # NOTE: 'pwd' will succeed even if the directory has disappeared. Other
355 # shells behave that way too.
356 pwd = self.mem.pwd
357
358 # '-L' is the default behavior; no need to check it
359 # TODO: ensure that if multiple flags are provided, the *last* one overrides
360 # the others
361 if arg.P:
362 pwd = libc.realpath(pwd)
363 print(pwd)
364 return 0