Interactive shell with Python Cmd ========================================== We are using Python's core module ``cmd``. Basic shell ------------ Let's start with a "Hello world!" example. Our interactive shell provides two commands: - ``greet`` - print a greeting. - ``bye`` - finish the interactive shell session. .. raw:: html To create a shell, start with creating a subclass to the ``Cmd`` class from the ``cmd`` module. .. code-block:: python import cmd class BasicShell(cmd.Cmd): pass if __name__ == "__main__": BasicShell().cmdloop() This is it! We already have an interactive shell. Well, it is not providing much functionality. It has only one command: ``help`` (and ``?`` as shortcut to ``help``). To exit the shell, you need to use :kbd:`Ctrl` + :kbd:`C`. We can run the shell: .. code-block:: console $ python basic_shell.py (Cmd) help Documented commands (type help ): ======================================== help (Cmd) Traceback (most recent call last): File "c:\Sandbox\PoC\python-repl-cmd\src\pacan\basic_shell.py", line 31, in BasicShell().cmdloop() File "C:\Users\ivang\AppData\Local\Programs\Python\Python310\lib\cmd.py", line 126, in cmdloop line = input(self.prompt) KeyboardInterrupt ^C To add command, we implement a method, named as ``do_``. For example to add a ``bye`` command that exits the shell, we need to create a ``do_bye`` method: .. code-block:: python def do_bye(self, argline:str) -> bool: return True Command method takes two arguments: - ``self`` - reference to the class instance. - ``argline`` - string containing the command arguments. This is the user input after the command. For example if user input is ``"great Ivan"``, the ``do_great`` method will receive ``Ivan`` as command argument. Command method **return value** is evaluated to boolean to decide if the shell session should be terminated. If result evaluates to ``True``, session is terminated. Otherwise, a prompt is shown and the shell awaits next user command. Change the prompt ------------------ The default prompt is `(cmd)`. To change it, assign corresponding value to the ``prompt`` attribute: .. code-block:: python # ... class BasicShell(cmd.Cmd): prompt = "> " # ... And try it: .. code-block:: console $ python basic_shell.py > greet Ivan Hello, Ivan! > bye Bye! As you can see the prompt has changed from default ``"(cmd)""`` to ``"> "``. Welcome message ----------------- We want when our interactive shell is started, to print the welcome message ``"Welcome to BasicShell! For help type `?` or `help`."`` (`gist `__): .. code-block::python class BasicShell(cmd.Cmd): """Interactive shell example.""" intro = "Welcome to BasicShell! For help type `?` or `help`.\n" prompt = "> " We can now start the interactive shell: .. code-block:: console Welcome to BasicShell! For help type `?` or `help`. > eval 112*2 224 > bye Bye! Empty command -------------- When the user enters an empty line, the default behavior is to execute the last executed command. We want to modify this by adding a message showing the command being executed. To implement we need to override the ``emptyline()`` method (`basic_shell_repeat.py gist `__): .. code-block:: python def emptyline(self): """Re-execute the last command""" print(f"REPEAT: {self.lastcmd}") super().emptyline() The last executed command is stored in the `lastcmd`. Trying the above approach: .. code-block:: console $ python basic_shell_repeat.py Welcome to BasicShell! For help type `?` or `help`. > eval 112*2 224 > REPEAT: eval 112*2 224 > bye Bye! Command arguments ------------------- Let's add a command ``eval`` which evaluates the expression given as parameter to the command (`basic_shell_eval.py gist `__): .. code-block:: python def do_eval(self, argline): try: print(eval(argline)) except Exception as error: print(f"ERROR EVALUATING {argline}: {error}") and try it: .. code-block:: console $ python basic_shell_prompt.py > eval 3+2 5 > eval 2**3 8 > bye Bye! Running shell command ---------------------- The special command ``!`` is a shortcut to the ``shell``. Let's implement a ``shell`` command (`basic_shell_shell.py `__): .. code-block:: python def do_shell(self, argline): """Execute a shell command and print the output.""" output = os.popen(argline).read() print(output) Let's try it: .. code-block:: console $ python basic_shell_shell.py Welcome to BasicShell! For help type `?` or `help`. > !dir Volume in drive C is OS Volume Serial Number is B89A-B1F9 Directory of C:\Sandbox\PoC\python-repl-cmd 12/22/2021 09:15 PM . 12/22/2021 09:15 PM .. 12/22/2021 09:14 PM 280 .dccache 12/22/2021 09:15 PM src 1 File(s) 280 bytes 3 Dir(s) 23,383,117,824 bytes free > bye Bye! Exit the shell on EOF character -------------------------------- By default the interative shell is not processing the ``EOF`` character. To process it, implement the ``EOF`` command (`basic_shell_eof.py `__): .. code-block:: python def do_EOF(self, argline): """Exit the shell.""" return self.do_bye(argline) There is more -------------- The ``cmd.Cmd`` class provides other customization options like pre- and post- command hooks, pre- and post- command loop hooks, help customization etc. You can find more information in the module `documentation `__. Summary -------- Let's summarize what we've just learned: 1. To create interactive shell, subclass the ``Cmd`` class from core ``cmd`` module. 2. To add a command to the shell, create a ``do_`` method to the shell class. 3. To exit the shell session, the command method should return ``False``. 4. The command method's docstring is used as help text by the shell. 5. To change the shell prompt, assign new value to the ``prompt`` attribute of the shell object. 6. To add welcome message, assign value to the ``intro`` attribute. 7. By default empty command (line) re-executes the last non-empty command. To change this, override the ``emptyline()`` method. 8. Commad method receives the user input as a second parameter. 9. To enable shell commands, implement a ``shell`` command (``do_shell()`` method). ``!`` is a shortcut for the ``shell`` command. 10. To process the EOF character, implement the ``EOF`` command (``do_EOF() method``).