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

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