diff --git a/README.rst b/README.rst index 339dc65..cd131e7 100644 --- a/README.rst +++ b/README.rst @@ -22,12 +22,30 @@ Run commands as you would in bash:: bash.pyc tests.pyc -Chain commands for the same effect:: +Pass in arguments as an array:: + + >>> bash('ls', ['.']) + bash.pyc + tests.pyc + +Chain commands for the same pipe effect as above:: >>> bash('ls . ').bash('grep ".pyc"') bash.pyc tests.pyc +Also chain using the ``xargs`` function to map the results of the previous command onto a new command:: + + >>> bash('ls').bash('grep "\.py"').xargs('grep "author=\'Alex Couper\'"') + 'setup.py: author=\'Alex Couper\',' + +Equivalently:: + + >>> files = [f for f in bash('ls').bash('grep "\.py"')] + >>> bash('grep "author=\'Alex Couper\'"', files) + 'setup.py: author=\'Alex Couper\',' + + This becomes increasingly useful if you later need to reuse one such command:: >>> b = bash('ls . ') @@ -60,6 +78,15 @@ To get a stripped, unicode string version of bash.stdout call value():: >>> b = bash('ls tests.py').value() u'tests.py' +To get the results (separated by newlines) as a list:: + + >>> b = bash('ls . ').results() + ['bash.pyc', 'tests.pyc'] + +or use the iterator directly:: + + >>> b = [res for res in bash('ls . ')] + ['bash.pyc', 'tests.pyc'] Motivation ---------- diff --git a/bash/__init__.py b/bash/__init__.py index d8da050..1519482 100644 --- a/bash/__init__.py +++ b/bash/__init__.py @@ -1,3 +1,4 @@ +import re import sys from subprocess import PIPE, Popen SUBPROCESS_HAS_TIMEOUT = True @@ -9,6 +10,8 @@ # will mean you don't have access to things like timeout SUBPROCESS_HAS_TIMEOUT = False +SPLIT_NEWLINE_REGEX = re.compile(' *\n *') + class bash(object): "This is lower class because it is intended to be used as a method." @@ -18,7 +21,8 @@ def __init__(self, *args, **kwargs): self.stdout = None self.bash(*args, **kwargs) - def bash(self, cmd, env=None, stdout=PIPE, stderr=PIPE, timeout=None, sync=True): + def bash(self, *cmds, env=None, stdout=PIPE, stderr=PIPE, timeout=None, sync=True): + cmd = bash.unpackCommands(cmds) self.p = Popen( cmd, shell=True, stdout=stdout, stdin=PIPE, stderr=stderr, env=env ) @@ -39,6 +43,14 @@ def sync(self, timeout=None): self.code = self.p.returncode return self + def xargs(self, *cmds, **kwargs): + bash.commandChecker(cmds) + args = cmds[1] if bash.areMultipleArgs(cmds) else [] + xargs = self.results() + passThroughCmds = [cmds[0], [*args, *xargs]] + print(passThroughCmds) + return self.bash(*passThroughCmds, **kwargs) + def __repr__(self): return self.value() @@ -54,7 +66,67 @@ def __nonzero__(self): def __bool__(self): return bool(self.value()) + def __iter__(self): + return self.results().__iter__() + def value(self): if self.stdout: return self.stdout.strip().decode(encoding='UTF-8') return '' + + def results(self): + output = self.stdout.decode(encoding='UTF-8').strip() or '' + if output: + return SPLIT_NEWLINE_REGEX.split(output) + else: + return [] + + @staticmethod + def areArgs(cmds): + return len(cmds) > 0 + + @staticmethod + def areMultipleArgs(cmds): + return len(cmds) > 1 + + @staticmethod + def commandChecker(cmds): + raisedError = None + try: + if not bash.areArgs(cmds): + raise SyntaxError('no arguments') + + masterCommand = cmds[0] + if not isinstance(masterCommand, str): + raise SyntaxError('first argument to bash must be a command as a string') + + if bash.areMultipleArgs(cmds): + arguments = cmds[1] + if not isinstance(arguments, list): + raise SyntaxError('second argument to bash (if specified) must be a list of strings') + areStrings = list(map(lambda el : isinstance(el, str), arguments)) + nonStrings = list(filter(lambda isString : not isString, areStrings)) + if len(nonStrings): + raise SyntaxError('one or more command arguments were not strings') + + if len(cmds) > 2: + raise SyntaxError('more than two bash arguments given') + + except SyntaxError as e: + raisedError = e + + finally: + if isinstance(raisedError, Exception): + raise SyntaxError( + str(raisedError) + '\n' + 'bash and xargs will accept one or two arguments: [command , arguments ]' + ) + + @staticmethod + def unpackCommands(cmds): + bash.commandChecker(cmds) + masterCommand = cmds[0] + arguments = cmds[1] if bash.areMultipleArgs(cmds) else [] + seperator = ' ' + argumentsString = seperator.join(arguments) + return masterCommand + ' ' + argumentsString diff --git a/tests.py b/tests.py index 41263b4..9945b61 100644 --- a/tests.py +++ b/tests.py @@ -66,3 +66,74 @@ def test_sync_false_does_not_wait(self): self.assertTrue((t2-t1).total_seconds() < 0.5) b.sync() self.assertEqual(b.stdout, b'1\n') + + def test_iterate_over_results(self): + expecting = ['setup.py', 'tests.py'] + b = bash('ls . | grep "\.py"') + results = b.results() + self.assertEqual(results, expecting) + + iteratedResults = [result for result in b] + self.assertEqual(iteratedResults, expecting) + + def test_accept_args_list(self): + expecting = ['setup.py', 'tests.py'] + b = bash('ls').bash('grep', ['-e', '"\.py"']) + results = b.results() + self.assertEqual(results, expecting) + + def test_syntax_error_no_args(self): + with self.assertRaises(SyntaxError) as e: + bash() + + self.assertEqual( + str(e.exception), + 'no arguments\n' + + 'bash and xargs will accept one or two arguments: [command , arguments ]' + ) + + def test_syntax_error_not_string_arg(self): + with self.assertRaises(SyntaxError) as e: + bash(1) + + self.assertEqual( + str(e.exception), + 'first argument to bash must be a command as a string\n' + + 'bash and xargs will accept one or two arguments: [command , arguments ]' + ) + + def test_syntax_error_second_arg_not_list(self): + with self.assertRaises(SyntaxError) as e: + bash('a', 'b') + + self.assertEqual( + str(e.exception), + 'second argument to bash (if specified) must be a list of strings\n' + + 'bash and xargs will accept one or two arguments: [command , arguments ]' + ) + + def test_syntax_error_second_arg_not_list_of_strings(self): + with self.assertRaises(SyntaxError) as e: + bash('a', [1]) + + self.assertEqual( + str(e.exception), + 'one or more command arguments were not strings\n' + + 'bash and xargs will accept one or two arguments: [command , arguments ]' + ) + + def test_syntax_error_second_arg_not_list_of_strings(self): + with self.assertRaises(SyntaxError) as e: + bash('a', ['b'], 'c') + + self.assertEqual( + str(e.exception), + 'more than two bash arguments given\n' + + 'bash and xargs will accept one or two arguments: [command , arguments ]' + ) + + def test_xargs(self): + expecting = 'setup.py: author=\'Alex Couper\',' + result = bash('ls').bash('grep', ['-e', '"\.py"']).xargs('grep', ['"author=\'Alex Couper\'"']).value() + self.assertEqual(result, expecting) +