OILS / builtin / dirs_osh.py View on Github | oilshell.org

346 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 try:
115 dest_dir = state.GetString(self.mem, 'HOME')
116 except error.Runtime as e:
117 self.errfmt.Print_(e.UserErrorString())
118 return 1
119
120 # At most 1 arg is accepted
121 arg_r.Next()
122 extra, extra_loc = arg_r.Peek2()
123 if extra is not None:
124 raise error.Usage('got too many arguments', extra_loc)
125
126 if dest_dir == '-':
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 new directory, chdir() to it, then set PWD to it. NOTE: We
138 # can't call posix.getcwd() because it can raise OSError if the
139 # directory was removed (ENOENT.)
140 abspath = os_path.join(old_pwd, dest_dir) # make it absolute, for cd ..
141 if arg.P:
142 # -P means resolve symbolic links, then process '..'
143 real_dest_dir = libc.realpath(abspath)
144 else:
145 # -L means process '..' first. This just does string manipulation.
146 # (But realpath afterward isn't correct?)
147 real_dest_dir = os_path.normpath(abspath)
148
149 err_num = pyos.Chdir(real_dest_dir)
150 if err_num != 0:
151 self.errfmt.Print_("cd %r: %s" %
152 (real_dest_dir, posix.strerror(err_num)),
153 blame_loc=arg_loc)
154 return 1
155
156 state.ExportGlobalString(self.mem, 'PWD', real_dest_dir)
157
158 # WEIRD: We need a copy that is NOT PWD, because the user could mutate
159 # PWD. Other shells use global variables.
160 self.mem.SetPwd(real_dest_dir)
161
162 if cmd_frag:
163 out_errs = [] # type: List[bool]
164 with ctx_CdBlock(self.dir_stack, real_dest_dir, self.mem,
165 self.errfmt, out_errs):
166 unused = self.cmd_ev.EvalCommandFrag(cmd_frag)
167 if len(out_errs):
168 return 1
169
170 else: # No block
171 state.ExportGlobalString(self.mem, 'OLDPWD', old_pwd)
172 self.dir_stack.Replace(real_dest_dir) # for pushd/popd/dirs
173
174 return 0
175
176
177WITH_LINE_NUMBERS = 1
178WITHOUT_LINE_NUMBERS = 2
179SINGLE_LINE = 3
180
181
182def _PrintDirStack(dir_stack, style, home_dir):
183 # type: (DirStack, int, Optional[str]) -> None
184 """ Helper for 'dirs' builtin """
185
186 if style == WITH_LINE_NUMBERS:
187 for i, entry in enumerate(dir_stack.Iter()):
188 print('%2d %s' % (i, ui.PrettyDir(entry, home_dir)))
189
190 elif style == WITHOUT_LINE_NUMBERS:
191 for entry in dir_stack.Iter():
192 print(ui.PrettyDir(entry, home_dir))
193
194 elif style == SINGLE_LINE:
195 parts = [ui.PrettyDir(entry, home_dir) for entry in dir_stack.Iter()]
196 s = ' '.join(parts)
197 print(s)
198
199
200class Pushd(vm._Builtin):
201
202 def __init__(self, mem, dir_stack, errfmt):
203 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
204 self.mem = mem
205 self.dir_stack = dir_stack
206 self.errfmt = errfmt
207
208 def Run(self, cmd_val):
209 # type: (cmd_value.Argv) -> int
210 _, arg_r = flag_util.ParseCmdVal('pushd', cmd_val)
211
212 dir_arg, dir_arg_loc = arg_r.Peek2()
213 if dir_arg is None:
214 # TODO: It's suppose to try another dir before doing this?
215 self.errfmt.Print_('pushd: no other directory')
216 # bash oddly returns 1, not 2
217 return 1
218
219 arg_r.Next()
220 extra, extra_loc = arg_r.Peek2()
221 if extra is not None:
222 e_usage('got too many arguments', extra_loc)
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
305 # Following bash order of flag priority
306 if arg.l:
307 home_dir = None # disable pretty ~
308 if arg.c:
309 self.dir_stack.Reset()
310 return 0
311 elif arg.v:
312 style = WITH_LINE_NUMBERS
313 elif arg.p:
314 style = WITHOUT_LINE_NUMBERS
315
316 _PrintDirStack(self.dir_stack, style, home_dir)
317 return 0
318
319
320class Pwd(vm._Builtin):
321 """
322 NOTE: pwd doesn't just call getcwd(), which returns a "physical" dir (not a
323 symlink).
324 """
325
326 def __init__(self, mem, errfmt):
327 # type: (state.Mem, ui.ErrorFormatter) -> None
328 self.mem = mem
329 self.errfmt = errfmt
330
331 def Run(self, cmd_val):
332 # type: (cmd_value.Argv) -> int
333 attrs, arg_r = flag_util.ParseCmdVal('pwd', cmd_val)
334 arg = arg_types.pwd(attrs.attrs)
335
336 # NOTE: 'pwd' will succeed even if the directory has disappeared. Other
337 # shells behave that way too.
338 pwd = self.mem.pwd
339
340 # '-L' is the default behavior; no need to check it
341 # TODO: ensure that if multiple flags are provided, the *last* one overrides
342 # the others
343 if arg.P:
344 pwd = libc.realpath(pwd)
345 print(pwd)
346 return 0