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

348 lines, 220 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 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 # Note: $OLDPWD isn't an env var; it's a global
128 try:
129 dest_dir = state.GetString(self.mem, 'OLDPWD')
130 print(dest_dir) # Shells print the directory
131 except error.Runtime as e:
132 self.errfmt.Print_(e.UserErrorString())
133 return 1
134
135 # Save a copy
136 old_pwd = self.mem.pwd
137
138 # Calculate new directory, chdir() to it, then set PWD to it. NOTE: We
139 # can't call posix.getcwd() because it can raise OSError if the
140 # directory was removed (ENOENT.)
141 abspath = os_path.join(old_pwd,
142 dest_dir) # make it absolute, for cd ..
143 if arg.P:
144 # -P means resolve symbolic links, then process '..'
145 real_dest_dir = libc.realpath(abspath)
146 else:
147 # -L means process '..' first. This just does string manipulation.
148 # (But realpath afterward isn't correct?)
149 real_dest_dir = os_path.normpath(abspath)
150
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 extra, extra_loc = arg_r.Peek2()
223 if extra is not None:
224 e_usage('got too many arguments', extra_loc)
225
226 # TODO: 'cd' uses normpath? Is that inconsistent?
227 dest_dir = os_path.abspath(dir_arg)
228 err_num = pyos.Chdir(dest_dir)
229 if err_num != 0:
230 self.errfmt.Print_("pushd: %r: %s" %
231 (dest_dir, posix.strerror(err_num)),
232 blame_loc=dir_arg_loc)
233 return 1
234
235 self.dir_stack.Push(dest_dir)
236 _PrintDirStack(self.dir_stack, SINGLE_LINE,
237 state.MaybeString(self.mem, 'HOME'))
238 state.ExportGlobalString(self.mem, 'PWD', dest_dir)
239 self.mem.SetPwd(dest_dir)
240 return 0
241
242
243def _PopDirStack(label, mem, dir_stack, errfmt, out_errs):
244 # type: (str, state.Mem, DirStack, ui.ErrorFormatter, List[bool]) -> bool
245 """ Helper for popd and cd { ... } """
246 dest_dir = dir_stack.Pop()
247 if dest_dir is None:
248 errfmt.Print_('%s: directory stack is empty' % label)
249 out_errs.append(True) # "return" to caller
250 return False
251
252 err_num = pyos.Chdir(dest_dir)
253 if err_num != 0:
254 # Happens if a directory is deleted in pushing and popping
255 errfmt.Print_('%s: %r: %s' %
256 (label, dest_dir, posix.strerror(err_num)))
257 out_errs.append(True) # "return" to caller
258 return False
259
260 state.SetGlobalString(mem, 'PWD', dest_dir)
261 mem.SetPwd(dest_dir)
262 return True
263
264
265class Popd(vm._Builtin):
266
267 def __init__(self, mem, dir_stack, errfmt):
268 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
269 self.mem = mem
270 self.dir_stack = dir_stack
271 self.errfmt = errfmt
272
273 def Run(self, cmd_val):
274 # type: (cmd_value.Argv) -> int
275 _, arg_r = flag_util.ParseCmdVal('pushd', cmd_val)
276
277 extra, extra_loc = arg_r.Peek2()
278 if extra is not None:
279 e_usage('got extra argument', extra_loc)
280
281 out_errs = [] # type: List[bool]
282 _PopDirStack('popd', self.mem, self.dir_stack, self.errfmt, out_errs)
283 if len(out_errs):
284 return 1 # error
285
286 _PrintDirStack(self.dir_stack, SINGLE_LINE,
287 state.MaybeString(self.mem, ('HOME')))
288 return 0
289
290
291class Dirs(vm._Builtin):
292
293 def __init__(self, mem, dir_stack, errfmt):
294 # type: (state.Mem, DirStack, ui.ErrorFormatter) -> None
295 self.mem = mem
296 self.dir_stack = dir_stack
297 self.errfmt = errfmt
298
299 def Run(self, cmd_val):
300 # type: (cmd_value.Argv) -> int
301 attrs, arg_r = flag_util.ParseCmdVal('dirs', cmd_val)
302 arg = arg_types.dirs(attrs.attrs)
303
304 home_dir = state.MaybeString(self.mem, 'HOME')
305 style = SINGLE_LINE
306
307 # Following bash order of flag priority
308 if arg.l:
309 home_dir = None # disable pretty ~
310 if arg.c:
311 self.dir_stack.Reset()
312 return 0
313 elif arg.v:
314 style = WITH_LINE_NUMBERS
315 elif arg.p:
316 style = WITHOUT_LINE_NUMBERS
317
318 _PrintDirStack(self.dir_stack, style, home_dir)
319 return 0
320
321
322class Pwd(vm._Builtin):
323 """
324 NOTE: pwd doesn't just call getcwd(), which returns a "physical" dir (not a
325 symlink).
326 """
327
328 def __init__(self, mem, errfmt):
329 # type: (state.Mem, ui.ErrorFormatter) -> None
330 self.mem = mem
331 self.errfmt = errfmt
332
333 def Run(self, cmd_val):
334 # type: (cmd_value.Argv) -> int
335 attrs, arg_r = flag_util.ParseCmdVal('pwd', cmd_val)
336 arg = arg_types.pwd(attrs.attrs)
337
338 # NOTE: 'pwd' will succeed even if the directory has disappeared. Other
339 # shells behave that way too.
340 pwd = self.mem.pwd
341
342 # '-L' is the default behavior; no need to check it
343 # TODO: ensure that if multiple flags are provided, the *last* one overrides
344 # the others
345 if arg.P:
346 pwd = libc.realpath(pwd)
347 print(pwd)
348 return 0