From 26cb8a5ed7236e035e97877af0b9d0e1685442f2 Mon Sep 17 00:00:00 2001 From: Ian Adam Naval Date: Thu, 26 Feb 2015 16:47:39 -0500 Subject: [PATCH 1/6] Restructure to Python package --- .gitignore | 1 + README.md | 44 ++++++++++++++++++++++++++ psh/__init__.py | 0 commands.py => psh/commands.py | 0 console.py => psh/console.py | 4 +-- example_cmd.py => psh/example_cmd.py | 2 +- formatters.py => psh/formatters.py | 2 +- raw_commands.py => psh/raw_commands.py | 4 +-- main.py => psh/run.py | 6 ++-- tree.py => psh/tree.py | 0 requirements.txt | 1 + setup.py | 20 ++++++++++++ 12 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 README.md create mode 100644 psh/__init__.py rename commands.py => psh/commands.py (100%) rename console.py => psh/console.py (97%) rename example_cmd.py => psh/example_cmd.py (94%) rename formatters.py => psh/formatters.py (89%) rename raw_commands.py => psh/raw_commands.py (93%) rename main.py => psh/run.py (81%) rename tree.py => psh/tree.py (100%) create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 8d35cb3..e581009 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ *.pyc +*.egg-info diff --git a/README.md b/README.md new file mode 100644 index 0000000..abadc96 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +psh +=== + +Augmented Unix Userland shell inspired by Windows PowerShell, written in Python. + +Requirements +------------ + +* Python 3+ +* pip + +Installing +---------- + +Preferably, you would use a separate virtual env + +``` +pip install -r requirements.txt +pip install -e . # installs the 'psh' package in editable mode +``` + +Running +------- + +From Python shell: + +``` +from psh.run import main +main() +``` + +From Unix shell: +``` +python -m psh.run +``` + +Testing +------- + +From Unix shell: + +``` +py.test +``` diff --git a/psh/__init__.py b/psh/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commands.py b/psh/commands.py similarity index 100% rename from commands.py rename to psh/commands.py diff --git a/console.py b/psh/console.py similarity index 97% rename from console.py rename to psh/console.py index b08f42d..06ffee7 100644 --- a/console.py +++ b/psh/console.py @@ -4,8 +4,8 @@ import os import readline import shlex -from commands import registered_cmds -import example_cmd +from psh.commands import registered_cmds +import psh.example_cmd DEFAULT_HISTORY_FILE = "~/.psh_history" diff --git a/example_cmd.py b/psh/example_cmd.py similarity index 94% rename from example_cmd.py rename to psh/example_cmd.py index 1b23ad3..8abf205 100644 --- a/example_cmd.py +++ b/psh/example_cmd.py @@ -1,4 +1,4 @@ -from commands import BaseCommand, register_cmd +from psh.commands import BaseCommand, register_cmd @register_cmd diff --git a/formatters.py b/psh/formatters.py similarity index 89% rename from formatters.py rename to psh/formatters.py index 8a516fb..3e50c47 100644 --- a/formatters.py +++ b/psh/formatters.py @@ -1,4 +1,4 @@ -from commands import BaseCommand +from psh.commands import BaseCommand class Printer(BaseCommand): diff --git a/raw_commands.py b/psh/raw_commands.py similarity index 93% rename from raw_commands.py rename to psh/raw_commands.py index 82f4c54..6777bc1 100644 --- a/raw_commands.py +++ b/psh/raw_commands.py @@ -1,5 +1,5 @@ -from formatters import Printer -from commands import BaseCommand +from psh.formatters import Printer +from psh.commands import BaseCommand class RawCommand(BaseCommand): diff --git a/main.py b/psh/run.py similarity index 81% rename from main.py rename to psh/run.py index a7bf3cc..2118e04 100644 --- a/main.py +++ b/psh/run.py @@ -1,8 +1,8 @@ import os import os.path -from formatters import * -from raw_commands import RawCommand +from psh.formatters import * +from psh.raw_commands import RawCommand # Load all of the commands in the path into the global namespace as raw # commands. @@ -15,7 +15,7 @@ for path in os.environ['PATH'].split(':'): def main(): - from console import HistoryConsole + from psh.console import HistoryConsole console = HistoryConsole(globals()) console.interact("Augmented Unix Userland") diff --git a/tree.py b/psh/tree.py similarity index 100% rename from tree.py rename to psh/tree.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a6fa282 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +import os +from setuptools import setup + +# Utility function to read the README file. +# Used for the long_description. It's nice, because now 1) we have a top level +# README file and 2) it's easier to type in the README file than to put a raw +# string in below ... +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = "psh", + version = "0.0.1", + author = "WPI Augmented Unix Userland MQP", + author_email = "jsh@wpi.edu", + description = ("Simple Unix shell inspired by PowerShell"), + license = "MIT", + packages=['psh'], + long_description=read('README.md'), +) From 7e708ba4ab1f599a9b5b121d09356731d157ed22 Mon Sep 17 00:00:00 2001 From: Ian Adam Naval Date: Thu, 26 Feb 2015 17:47:06 -0500 Subject: [PATCH 2/6] Made registering commands better Registering commands requires adding the name as a string so that we can have nice PEP 8-compatible class names for commands. --- psh/__init__.py | 11 +++++++++++ psh/commands.py | 12 +++++++----- psh/example_cmd.py | 12 ++++++------ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/psh/__init__.py b/psh/__init__.py index e69de29..6bd963c 100644 --- a/psh/__init__.py +++ b/psh/__init__.py @@ -0,0 +1,11 @@ +from psh.commands import registered_cmds + +# Import the exported commands +from psh.example_cmd import * + +# Instantiate the registered commands +for name, cls in registered_cmds.items(): + globals()[name] = cls() + +# Only export the names of registered commands +__all__ = registered_cmds.keys() diff --git a/psh/commands.py b/psh/commands.py index ab41676..164fd58 100644 --- a/psh/commands.py +++ b/psh/commands.py @@ -38,9 +38,11 @@ class BaseCommand(object): return cmd -registered_cmds = [] +registered_cmds = {} -def register_cmd(cls): - """Decorator for putting all of the commands in one nice place.""" - registered_cmds.append(cls.__name__) - return cls +def register_cmd(name): + def decorator(cls): + """Decorator for putting all of the commands in one nice place.""" + registered_cmds[name] = cls + return cls + return decorator diff --git a/psh/example_cmd.py b/psh/example_cmd.py index 8abf205..897dda7 100644 --- a/psh/example_cmd.py +++ b/psh/example_cmd.py @@ -1,8 +1,8 @@ from psh.commands import BaseCommand, register_cmd -@register_cmd -class example_cmd(BaseCommand): +@register_cmd("example") +class Example(BaseCommand): """Simple command that just returns 'example' and 'command'. Does nothing at all with the input.""" @@ -13,13 +13,13 @@ class example_cmd(BaseCommand): return output_generator -@register_cmd -class echo(BaseCommand): +@register_cmd("echo") +class Echo(BaseCommand): """Echoes anything from the command line arguments as well as input from the previous command.""" - def __init__(self, args): - super(echo, self).__init__() + def __init__(self, args=[]): + super(Echo, self).__init__() self.args = args def call(self,*args,**kwargs): From 7b5b8b4d06e6c348ca78bfd3287af39c120f805e Mon Sep 17 00:00:00 2001 From: Ian Adam Naval Date: Thu, 26 Feb 2015 18:07:50 -0500 Subject: [PATCH 3/6] Make echo and example available in new shell --- psh/console.py | 4 ++-- psh/example_cmd.py | 1 - psh/run.py | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/psh/console.py b/psh/console.py index 06ffee7..cf649d7 100644 --- a/psh/console.py +++ b/psh/console.py @@ -5,7 +5,6 @@ import readline import shlex from psh.commands import registered_cmds -import psh.example_cmd DEFAULT_HISTORY_FILE = "~/.psh_history" @@ -26,7 +25,8 @@ def parse_cmd(potential_cmd): if cmd_name not in registered_cmds: return "RawCommand({})".format(shlex.split(potential_cmd)) else: - return "{0}({1})".format(cmd_name,str(args)) + cls = registered_cmds[cmd_name].__name__ + return "{0}({1})".format(cls, str(args)) def parse_cmds(raw_input_line): diff --git a/psh/example_cmd.py b/psh/example_cmd.py index 897dda7..e54e408 100644 --- a/psh/example_cmd.py +++ b/psh/example_cmd.py @@ -30,4 +30,3 @@ class Echo(BaseCommand): for line in input_generator: yield line return output_generator - diff --git a/psh/run.py b/psh/run.py index 2118e04..a8b79a7 100644 --- a/psh/run.py +++ b/psh/run.py @@ -2,6 +2,8 @@ import os import os.path from psh.formatters import * + +from psh.example_cmd import Echo, Example from psh.raw_commands import RawCommand # Load all of the commands in the path into the global namespace as raw From 1d7d2d84463ed16dd19e823b7d61865b96f79615 Mon Sep 17 00:00:00 2001 From: Ian Adam Naval Date: Thu, 26 Feb 2015 18:08:01 -0500 Subject: [PATCH 4/6] Add tests for example commands --- test/test_example_cmd.py | 19 +++++++++++++++++++ test/utils.py | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 test/test_example_cmd.py create mode 100644 test/utils.py diff --git a/test/test_example_cmd.py b/test/test_example_cmd.py new file mode 100644 index 0000000..8b24b18 --- /dev/null +++ b/test/test_example_cmd.py @@ -0,0 +1,19 @@ +import pytest + +from psh import echo, example +from utils import TestFormatter + + +@pytest.fixture +def test_formatter(): + return TestFormatter() + + +def test_example_cmd_should_return_two_things(test_formatter): + example.chain(test_formatter).call() + assert "examplecommand" == test_formatter.get_data() + + +def test_echo_should_echo(test_formatter): + example.chain(echo).chain(test_formatter).call() + assert "examplecommand" == test_formatter.get_data() diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 0000000..6c73571 --- /dev/null +++ b/test/utils.py @@ -0,0 +1,18 @@ +from psh.commands import BaseCommand + +from io import StringIO + +class TestFormatter(BaseCommand): + + def __init__(self, *args, **kwargs): + super(TestFormatter, self).__init__(*args, **kwargs) + self.buffer = StringIO() + + def call(self): + input_generator = self.get_input_generator() + for line in input_generator: + self.buffer.write(line.decode('utf-8')) + return None + + def get_data(self): + return self.buffer.getvalue() From 781ae4b99cbb3c1137697ac31100e43bbae4b4ea Mon Sep 17 00:00:00 2001 From: Ian Adam Naval Date: Thu, 26 Feb 2015 18:19:12 -0500 Subject: [PATCH 5/6] Moved shlex import to the raw command module Makes much more sense to do the parsing at that point. --- psh/console.py | 3 +-- psh/raw_commands.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/psh/console.py b/psh/console.py index cf649d7..491440f 100644 --- a/psh/console.py +++ b/psh/console.py @@ -2,7 +2,6 @@ import atexit import code import os import readline -import shlex from psh.commands import registered_cmds @@ -23,7 +22,7 @@ def parse_cmd(potential_cmd): if args: args = args[1:] if cmd_name not in registered_cmds: - return "RawCommand({})".format(shlex.split(potential_cmd)) + return "RawCommand('{}')".format(potential_cmd) else: cls = registered_cmds[cmd_name].__name__ return "{0}({1})".format(cls, str(args)) diff --git a/psh/raw_commands.py b/psh/raw_commands.py index 6777bc1..1d61444 100644 --- a/psh/raw_commands.py +++ b/psh/raw_commands.py @@ -1,3 +1,5 @@ +import shlex + from psh.formatters import Printer from psh.commands import BaseCommand @@ -15,7 +17,7 @@ class RawCommand(BaseCommand): input_generator = self.get_input_generator() import subprocess try: - p = subprocess.Popen(self.cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + p = subprocess.Popen(shlex.split(self.cmd), stdin=subprocess.PIPE, stdout=subprocess.PIPE) def make_output_generator(): input_str = b"" for line in input_generator: From f54ffc4136eca63f3bf957c1825c83f608f06b73 Mon Sep 17 00:00:00 2001 From: Ian Adam Naval Date: Thu, 26 Feb 2015 18:22:51 -0500 Subject: [PATCH 6/6] Add docs for test formatter --- test/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/utils.py b/test/utils.py index 6c73571..be7696c 100644 --- a/test/utils.py +++ b/test/utils.py @@ -3,6 +3,9 @@ from psh.commands import BaseCommand from io import StringIO class TestFormatter(BaseCommand): + """Formatter useful for tests. Instead of printing to stdout, it + stores any output inside a stringio buffer. This can be retrieved + with the get_data method.""" def __init__(self, *args, **kwargs): super(TestFormatter, self).__init__(*args, **kwargs)