import os
import cmd2
# Inspired from: http://pwet.fr/man/linux/commandes/posix/cd/
# Inspired by: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cd.html
cd_parser = cmd2.Cmd2ArgumentParser(
prog="cd",
description="Change the working directory",
)
cd_parser.add_argument(
"directory",
nargs="?",
const=0,
help="An absolute or relative pathname of the directory that shall become the new working directory.",
)
cd_parser.add_argument(
"-P",
dest="physical",
action="store_true",
default=False,
help="Symbolic link components shall be resolved before dot-dot components are processed",
)
cd_parser.add_argument(
"-L",
dest="logical",
action="store_true",
default=False,
help="Symbolic link components shall not be resolved before dot-dot components are processed",
)
[docs]class GLXCd(cmd2.CommandSet):
def __init__(self):
super().__init__()
self.__curpath = None
self.__directory = None
self.__logical = None
self.__physical = None
self.__can_continue = None
self.curpath = None
self.directory = None
self.logical = None
self.physical = None
self.can_continue = None
@property
def curpath(self):
"""
``curpath`` represents an intermediate value used to simplify the description of the algorithm used by cd.
:return: value used to simplify the description
:rtype: str or None
"""
return self.__curpath
@curpath.setter
def curpath(self, value=None):
"""
Set the ``curpath`` property value
:param value: value used to simplify the description
:type value: str or None
:raise TypeError: when ``directory`` property value is not str type or None
"""
if value is not None and type(value) != str:
raise TypeError("'curpath' property value must be str type or None")
if self.curpath != value:
self.__curpath = value
@property
def directory(self):
"""
An absolute or relative pathname of the directory that shall become the new working directory.
:return: directory that shall become the new working directory
:rtype: str or None
"""
return self.__directory
@directory.setter
def directory(self, value=None):
"""
Set the ``directory`` property value
:param value: directory that shall become the new working directory
:type value: str or None
:raise TypeError: when ``directory`` property value is not str type or None
"""
if value is not None and type(value) != str:
raise TypeError("'directory' property value must be str type or None")
if self.directory != value:
self.__directory = value
@property
def logical(self):
"""
The ``logical`` property is use as control during the 9 steps of the posix sequence
Default: False
:return: True if handle the operand dot-dot logically
:rtype: bool
"""
return self.__logical
@logical.setter
def logical(self, value=None):
"""
Set the ``logical`` property value
Default: False
:param value: True if handle the operand dot-dot logically
:type value: bool or None
"""
if value is None:
value = False
if type(value) != bool:
raise TypeError("'logical' property value must be bool type or None")
if self.logical != value:
self.__logical = value
@property
def physical(self):
"""
The ``physical`` property is use as control during the 9 steps of the posix sequence
Default: False
:return: True if handle the operand dot-dot physically
:rtype: bool
"""
return self.__physical
@physical.setter
def physical(self, value=None):
"""
Set the ``physical`` property value
Default: True
:param value: True if handle the operand dot-dot physically
:type value: bool or None
"""
if value is None:
value = False
if type(value) != bool:
raise TypeError("'physical' property value must be bool type or None")
if self.physical != value:
self.__physical = value
@property
def can_continue(self):
"""
The ``working`` property is use as control during the 9 steps of the posix sequence
Default: True
:return: True if any step can be operate
:rtype: bool
"""
return self.__can_continue
@can_continue.setter
def can_continue(self, value=None):
"""
Set the ``working`` property value
Default: True
:param value: True if any step can be operate
:type value: bool or None
"""
if value is None:
value = True
if type(value) != bool:
raise TypeError("'working' property value must be bool type or None")
if self.can_continue != value:
self.__can_continue = value
[docs] def cd(self, directory=None, logical=None, physical=None):
# Init
self.can_continue = True
self.curpath = None
self.directory = directory
self.logical = logical
self.physical = physical
# Logical
if self.logical and self.physical:
self.physical = False
if not self.logical and not self.physical:
self.logical = True
if self.directory == "-":
if os.environ.get("OLDPWD"):
self.directory = os.environ.get("OLDPWD")
else:
self.directory = os.environ.get("PWD")
elif self.directory is not None:
self.directory = os.path.expanduser(self.directory)
# Start the posix sequence
self._step_1()
self._step_2()
self._step_3()
self._step_4()
self._step_5()
self._step_6()
self._step_7()
self._step_8()
self._step_9()
# Inform cmd2 directly
cmd2.Cmd().last_result = os.getcwd()
cmd2.Cmd().set_window_title(
os.path.normpath(os.getcwd()).replace(
os.path.realpath(os.path.expanduser("~")), "~"
)
)
def _step_1(self):
"""
If no directory operand is given and the HOME environment variable is empty or undefined,
the default behavior is implementation-defined and no further steps shall be taken.
"""
if self.can_continue and self.directory is None and not os.environ.get("HOME"):
self.can_continue = False
def _step_2(self):
"""
If no directory operand is given and the HOME environment variable is set to a non-empty value,
the *cd* utility shall behave as if the directory named in the HOME environment variable was
specified as the directory operand.
"""
if self.can_continue and self.directory is None and os.environ.get("HOME"):
self.directory = os.environ.get("HOME")
def _step_3(self):
"""
If the directory operand begins with a slash character, set ``curpath`` to the operand and proceed to step 7.
"""
if self.can_continue and self.directory.startswith(os.path.sep):
self.curpath = self.directory
self._step_7()
def _step_4(self):
"""
If the first component of the directory operand is dot or dot-dot, proceed to step 6.
"""
if (
self.can_continue
and self.directory.startswith("..")
or self.directory.startswith(".")
):
self._step_6()
def _step_5(self):
"""
Starting with the first pathname in the colon-separated pathnames of CDPATH
(see the ENVIRONMENT VARIABLES section) if the pathname is non-null,
test if the concatenation of that pathname, a slash character, and the directory operand names a directory.
If the pathname is null, test if the concatenation of dot, a slash character, and the operand names a directory.
In either case, if the resulting string names an existing directory,
set curpath to that string and proceed to step 7.
Otherwise, repeat this step with the next pathname in CDPATH until all pathnames have been tested.
"""
# 5. Starting with the first pathname in the colon-separated pathnames of CDPATH
# (see the ENVIRONMENT VARIABLES section)
if self.can_continue and os.environ.get("CDPATH"):
for pathname in os.environ.get("CDPATH").split(":"):
if pathname:
# if the pathname is non-null, test if the concatenation of that pathname,
# a slash character, and the directory operand names a directory.
if os.path.isdir(os.path.join(pathname, self.directory)):
self.curpath = os.path.join(pathname, self.directory)
self._step_7()
break
# If the pathname is null, test if the concatenation of dot, a slash character, and
# the operand names a directory.
# set curpath to that string and proceed to step 7.
elif os.path.isdir(
os.path.join(".{0}".format(os.path.sep), self.directory)
):
self.curpath = os.path.join(
".{0}".format(os.path.sep), self.directory
)
self._step_7()
break
# elif os.path.isdir(pathname):
# # In either case, if the resulting string names an existing directory,
# self.curpath = pathname
# self._step_7()
# break
# Otherwise, repeat this step with the next pathname in CDPATH until all pathnames have been tested.
def _step_6(self):
"""
Set ``curpath`` to the string formed by the concatenation of the value of *PWD* , a slash character,
and the operand.
"""
if self.can_continue:
self.curpath = os.path.join(os.environ.get("PWD"), self.directory)
def _step_7(self):
"""
If the **-P** option is in effect, the cd utility shall perform actions equivalent to the chdir() function,
called with ``curpath`` as the path argument.
If these actions succeed, the *PWD* environment variable shall be set to an absolute pathname for
the current working directory and shall not contain filename components that,
in the context of pathname resolution, refer to a file of type symbolic link.
If there is insufficient permission on the new directory, or on any parent of that directory,
to determine the current working directory, the value of the *PWD* environment variable is unspecified.
If the actions equivalent to ``chdir()`` fail for any reason, the cd utility shall display
an appropriate error message and not alter the PWD environment variable.
Whether the actions equivalent to ``chdir()`` succeed or fail, no further steps shall be taken.
"""
if self.can_continue and self.physical:
# Make sure the directory exists, is a directory, and we have read access
err = None
if not os.path.isdir(self.curpath):
err = f"cd: {self.curpath}: No such file or directory"
elif not os.access(self.curpath, os.R_OK): # pragma: no cover
err = f"cd: {self.curpath}: Permission denied"
else:
try:
os.chdir(self.curpath)
except Exception as ex: # pragma: no cover
err = f"{ex}"
else:
if os.environ.get("OLDPWD") != os.environ["PWD"]:
os.environ["OLDPWD"] = os.environ["PWD"]
if os.environ.get("PWD") != os.path.normpath(os.getcwd()):
os.environ["PWD"] = os.path.normpath(os.getcwd())
if err:
cmd2.Cmd().perror(err)
self.can_continue = False
def _step_8(self):
"""
The ``curpath`` value shall then be converted to canonical form as follows, considering each component
from beginning to end, in sequence:
a. Dot components and any slashes that separate them from the next component shall be deleted.
b. For each dot-dot component, if there is a preceding component and it is neither root nor dot-dot,
the preceding component, all slashes separating the preceding component from dot-dot, dot-dot and all slashes
separating dot-dot from the following component shall be deleted.
c. An implementation may further simplify ``curpath`` by removing any trailing slash characters that are not
also leading slashes, replacing multiple non-leading consecutive slashes with a single slash, and replacing
three or more leading slashes with a single slash. If, as a result of this canonicalization, the ``curpath``
variable is null, no further steps shall be taken.
"""
if self.can_continue:
self.curpath = os.path.abspath(self.curpath)
def _step_9(self):
"""
The cd utility shall then perform actions equivalent to the ``chdir()`` function called with ``curpath`` as
the path argument.
If these actions failed for any reason, the cd utility shall display an appropriate error message
and no further steps shall be taken.
The *PWD* environment variable shall be set to ``curpath``.
"""
if self.can_continue:
err = None
if not os.path.isdir(self.curpath):
err = f"cd: {self.curpath}: No such file or directory"
elif not os.access(self.curpath, os.R_OK): # pragma: no cover
err = f"cd: {self.curpath}: Permission denied"
else:
try:
os.chdir(self.curpath)
except Exception as ex: # pragma: no cover
err = f"{ex}"
else:
if os.environ.get("OLDPWD") != os.environ["PWD"]:
os.environ["OLDPWD"] = os.environ["PWD"]
if os.environ["PWD"] != self.curpath:
os.environ["PWD"] = self.curpath
if err:
cmd2.Cmd().perror(err)
self.can_continue = False
[docs] @cmd2.with_argparser(cd_parser)
@cmd2.with_category("Builtins")
def do_cd(self, args):
self.cd(
directory=args.directory,
logical=args.logical,
physical=args.physical
) # pragma: no cover
[docs] @staticmethod
def complete_cd(text, line, begidx, endidx):
return cmd2.Cmd().path_complete(
text=text,
line=line,
begidx=begidx,
endidx=endidx,
path_filter=os.path.isdir,
)