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

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