Skip to content

PR:2 Pass in a list of command args as the second bash argument and xargs #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 . ')
Expand Down Expand Up @@ -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
----------
Expand Down
74 changes: 73 additions & 1 deletion bash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import sys
from subprocess import PIPE, Popen
SUBPROCESS_HAS_TIMEOUT = True
Expand All @@ -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."
Expand All @@ -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
)
Expand All @@ -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()

Expand All @@ -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 <str>, arguments <list of strings>]'
)

@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
71 changes: 71 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <str>, arguments <list of strings>]'
)

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 <str>, arguments <list of strings>]'
)

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 <str>, arguments <list of strings>]'
)

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 <str>, arguments <list of strings>]'
)

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 <str>, arguments <list of strings>]'
)

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)