From f0fab6e11fef88541f5a094b4174d1898ade51ee Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Sun, 12 Apr 2020 22:12:06 +0100 Subject: [PATCH 1/9] results function to return multiline output as array --- bash/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bash/__init__.py b/bash/__init__.py index d8da050..aa4d542 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." @@ -58,3 +61,10 @@ 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 [] From bdbe983455f2e9b498ba00b14d822c3484664601 Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Sun, 12 Apr 2020 23:58:05 +0100 Subject: [PATCH 2/9] allow iteration over result --- bash/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bash/__init__.py b/bash/__init__.py index aa4d542..2630fba 100644 --- a/bash/__init__.py +++ b/bash/__init__.py @@ -57,6 +57,9 @@ 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') From d1eada9795c1aaaf0b9a04ba956026b25d8bd961 Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Mon, 13 Apr 2020 00:12:46 +0100 Subject: [PATCH 3/9] add unit tests --- tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests.py b/tests.py index 41263b4..babc0de 100644 --- a/tests.py +++ b/tests.py @@ -66,3 +66,12 @@ 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) From 58ea69a5d0b99d54fe50d4a5a104133da68546b1 Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Mon, 13 Apr 2020 00:49:41 +0100 Subject: [PATCH 4/9] update readme --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 339dc65..73afaf8 100644 --- a/README.rst +++ b/README.rst @@ -60,6 +60,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 ---------- From c2d12724fa8298f0cd9603cda1eb0ce4feaf07a3 Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Sun, 12 Apr 2020 23:51:31 +0100 Subject: [PATCH 5/9] allow passing of arguments as an array --- bash/__init__.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/bash/__init__.py b/bash/__init__.py index 2630fba..ddc5675 100644 --- a/bash/__init__.py +++ b/bash/__init__.py @@ -21,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 ) @@ -71,3 +72,53 @@ def results(self): 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 From b55b8b2e1ecef64610629ac023ec2ab78a19a2f5 Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Sun, 12 Apr 2020 23:51:52 +0100 Subject: [PATCH 6/9] use array arguments to implement xargs --- bash/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bash/__init__.py b/bash/__init__.py index ddc5675..1519482 100644 --- a/bash/__init__.py +++ b/bash/__init__.py @@ -43,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() From a8503b62f3496b27722dd4ef68c84dcdf37fe8f0 Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Mon, 13 Apr 2020 00:45:29 +0100 Subject: [PATCH 7/9] add tests --- tests.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests.py b/tests.py index babc0de..9945b61 100644 --- a/tests.py +++ b/tests.py @@ -75,3 +75,65 @@ def test_iterate_over_results(self): 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) + From db6c2bfbae4873c9d06d576955264a91578d39f6 Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Mon, 13 Apr 2020 01:07:47 +0100 Subject: [PATCH 8/9] update readme --- README.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 73afaf8..311b7b1 100644 --- a/README.rst +++ b/README.rst @@ -22,12 +22,29 @@ 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 . ') From 8151450c871f0a1be5e4a1fa0890f1ca1c4ca71d Mon Sep 17 00:00:00 2001 From: Alan Bacon Date: Mon, 13 Apr 2020 01:09:24 +0100 Subject: [PATCH 9/9] fix typo in readme --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 311b7b1..cd131e7 100644 --- a/README.rst +++ b/README.rst @@ -40,6 +40,7 @@ Also chain using the ``xargs`` function to map the results of the previous comma '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\','