| 1 | from __future__ import print_function
 | 
| 2 | 
 | 
| 3 | from _devbuild.gen import arg_types
 | 
| 4 | from _devbuild.gen.runtime_asdl import cmd_value
 | 
| 5 | from core import error
 | 
| 6 | from core.error import e_usage
 | 
| 7 | from core import pyos
 | 
| 8 | from core import state
 | 
| 9 | from core import ui
 | 
| 10 | from core import vm
 | 
| 11 | from frontend import flag_util
 | 
| 12 | from frontend import typed_args
 | 
| 13 | from mycpp.mylib import log
 | 
| 14 | from pylib import os_path
 | 
| 15 | 
 | 
| 16 | import libc
 | 
| 17 | import posix_ as posix
 | 
| 18 | 
 | 
| 19 | from typing import List, Optional, Any, TYPE_CHECKING
 | 
| 20 | if TYPE_CHECKING:
 | 
| 21 |     from osh.cmd_eval import CommandEvaluator
 | 
| 22 | 
 | 
| 23 | _ = log
 | 
| 24 | 
 | 
| 25 | 
 | 
| 26 | class 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 | 
 | 
| 67 | class 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 | 
 | 
| 88 | class 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 | 
 | 
| 180 | WITH_LINE_NUMBERS = 1
 | 
| 181 | WITHOUT_LINE_NUMBERS = 2
 | 
| 182 | SINGLE_LINE = 3
 | 
| 183 | 
 | 
| 184 | 
 | 
| 185 | def _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 | 
 | 
| 203 | class 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 | 
 | 
| 244 | def _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 | 
 | 
| 266 | class 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 | 
 | 
| 292 | class 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 | 
 | 
| 323 | class 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
 |