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

349 lines, 223 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 = typed_args.OptionalBlock(cmd_val)
106
107 dest_dir, arg_loc = arg_r.Peek2()
108 if dest_dir is None:
109 if cmd:
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 try:
135 pwd = state.GetString(self.mem, 'PWD')
136 except error.Runtime as e:
137 self.errfmt.Print_(e.UserErrorString())
138 return 1
139
140 # Calculate new directory, chdir() to it, then set PWD to it. NOTE: We
141 # can't call posix.getcwd() because it can raise OSError if the
142 # directory was removed (ENOENT.)
143 abspath = os_path.join(pwd, dest_dir) # make it absolute, for cd ..
144 if arg.P:
145 # -P means resolve symbolic links, then process '..'
146 real_dest_dir = libc.realpath(abspath)
147 else:
148 # -L means process '..' first. This just does string manipulation.
149 # (But realpath afterward isn't correct?)
150 real_dest_dir = os_path.normpath(abspath)
151
152 err_num = pyos.Chdir(real_dest_dir)
153 if err_num != 0:
154 self.errfmt.Print_("cd %r: %s" %
155 (real_dest_dir, posix.strerror(err_num)),
156 blame_loc=arg_loc)
157 return 1
158
159 state.ExportGlobalString(self.mem, 'PWD', real_dest_dir)
160
161 # WEIRD: We need a copy that is NOT PWD, because the user could mutate
162 # PWD. Other shells use global variables.
163 self.mem.SetPwd(real_dest_dir)
164
165 if cmd:
166 out_errs = [] # type: List[bool]
167 with ctx_CdBlock(self.dir_stack, real_dest_dir, self.mem,
168 self.errfmt, out_errs):
169 unused = self.cmd_ev.EvalCommand(cmd)
170 if len(out_errs):
171 return 1
172
173 else: # No block
174 state.ExportGlobalString(self.mem, 'OLDPWD', pwd)
175 self.dir_stack.Replace(real_dest_dir) # for pushd/popd/dirs
176
177 return 0
178
179
180WITH_LINE_NUMBERS = 1
181WITHOUT_LINE_NUMBERS = 2
182SINGLE_LINE = 3
183
184
185def _PrintDirStack(dir_stack, style, home_dir):
186 # type: (DirStack, int, Optional[str]) -> None
187 """ Helper for 'dirs' builtin """
188
189 if style == WITH_LINE_NUMBERS:
190 for i, entry in enumerate(dir_stack.Iter()):
191 print('%2d %s' % (i, ui.PrettyDir(entry, home_dir)))
192
193 elif style == WITHOUT_LINE_NUMBERS:
194 for entry in dir_stack.Iter():
195 print(ui.PrettyDir(entry, home_dir))
196
197 elif style == SINGLE_LINE:
198 parts = [ui.PrettyDir(entry, home_dir) for entry in dir_stack.Iter()]
199 s = ' '.join(parts)
200 print(s)
201
202
203class Pushd(vm._Builtin):
204
205 def __init__(self, mem, dir_stack, errfmt):
206 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
207 self.mem = mem
208 self.dir_stack = dir_stack
209 self.errfmt = errfmt
210
211 def Run(self, cmd_val):
212 # type: (cmd_value.Argv) -> int
213 _, arg_r = flag_util.ParseCmdVal('pushd', cmd_val)
214
215 dir_arg, dir_arg_loc = arg_r.Peek2()
216 if dir_arg is None:
217 # TODO: It's suppose to try another dir before doing this?
218 self.errfmt.Print_('pushd: no other directory')
219 # bash oddly returns 1, not 2
220 return 1
221
222 arg_r.Next()
223 extra, extra_loc = arg_r.Peek2()
224 if extra is not None:
225 e_usage('got too many arguments', extra_loc)
226
227 # TODO: 'cd' uses normpath? Is that inconsistent?
228 dest_dir = os_path.abspath(dir_arg)
229 err_num = pyos.Chdir(dest_dir)
230 if err_num != 0:
231 self.errfmt.Print_("pushd: %r: %s" %
232 (dest_dir, posix.strerror(err_num)),
233 blame_loc=dir_arg_loc)
234 return 1
235
236 self.dir_stack.Push(dest_dir)
237 _PrintDirStack(self.dir_stack, SINGLE_LINE,
238 state.MaybeString(self.mem, 'HOME'))
239 state.ExportGlobalString(self.mem, 'PWD', dest_dir)
240 self.mem.SetPwd(dest_dir)
241 return 0
242
243
244def _PopDirStack(label, mem, dir_stack, errfmt, out_errs):
245 # type: (str, state.Mem, DirStack, ui.ErrorFormatter, List[bool]) -> bool
246 """ Helper for popd and cd { ... } """
247 dest_dir = dir_stack.Pop()
248 if dest_dir is None:
249 errfmt.Print_('%s: directory stack is empty' % label)
250 out_errs.append(True) # "return" to caller
251 return False
252
253 err_num = pyos.Chdir(dest_dir)
254 if err_num != 0:
255 # Happens if a directory is deleted in pushing and popping
256 errfmt.Print_('%s: %r: %s' %
257 (label, dest_dir, posix.strerror(err_num)))
258 out_errs.append(True) # "return" to caller
259 return False
260
261 state.SetGlobalString(mem, 'PWD', dest_dir)
262 mem.SetPwd(dest_dir)
263 return True
264
265
266class Popd(vm._Builtin):
267
268 def __init__(self, mem, dir_stack, errfmt):
269 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
270 self.mem = mem
271 self.dir_stack = dir_stack
272 self.errfmt = errfmt
273
274 def Run(self, cmd_val):
275 # type: (cmd_value.Argv) -> int
276 _, arg_r = flag_util.ParseCmdVal('pushd', cmd_val)
277
278 extra, extra_loc = arg_r.Peek2()
279 if extra is not None:
280 e_usage('got extra argument', extra_loc)
281
282 out_errs = [] # type: List[bool]
283 _PopDirStack('popd', self.mem, self.dir_stack, self.errfmt, out_errs)
284 if len(out_errs):
285 return 1 # error
286
287 _PrintDirStack(self.dir_stack, SINGLE_LINE,
288 state.MaybeString(self.mem, ('HOME')))
289 return 0
290
291
292class Dirs(vm._Builtin):
293
294 def __init__(self, mem, dir_stack, errfmt):
295 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
296 self.mem = mem
297 self.dir_stack = dir_stack
298 self.errfmt = errfmt
299
300 def Run(self, cmd_val):
301 # type: (cmd_value.Argv) -> int
302 attrs, arg_r = flag_util.ParseCmdVal('dirs', cmd_val)
303 arg = arg_types.dirs(attrs.attrs)
304
305 home_dir = state.MaybeString(self.mem, 'HOME')
306 style = SINGLE_LINE
307
308 # Following bash order of flag priority
309 if arg.l:
310 home_dir = None # disable pretty ~
311 if arg.c:
312 self.dir_stack.Reset()
313 return 0
314 elif arg.v:
315 style = WITH_LINE_NUMBERS
316 elif arg.p:
317 style = WITHOUT_LINE_NUMBERS
318
319 _PrintDirStack(self.dir_stack, style, home_dir)
320 return 0
321
322
323class Pwd(vm._Builtin):
324 """
325 NOTE: pwd doesn't just call getcwd(), which returns a "physical" dir (not a
326 symlink).
327 """
328
329 def __init__(self, mem, errfmt):
330 # type: (state.Mem, ui.ErrorFormatter) -> None
331 self.mem = mem
332 self.errfmt = errfmt
333
334 def Run(self, cmd_val):
335 # type: (cmd_value.Argv) -> int
336 attrs, arg_r = flag_util.ParseCmdVal('pwd', cmd_val)
337 arg = arg_types.pwd(attrs.attrs)
338
339 # NOTE: 'pwd' will succeed even if the directory has disappeared. Other
340 # shells behave that way too.
341 pwd = self.mem.pwd
342
343 # '-L' is the default behavior; no need to check it
344 # TODO: ensure that if multiple flags are provided, the *last* one overrides
345 # the others
346 if arg.P:
347 pwd = libc.realpath(pwd)
348 print(pwd)
349 return 0