summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2014-07-31 09:21:21 +0100
committerLars Wirzenius <liw@liw.fi>2014-07-31 09:21:21 +0100
commitf846d8c4628dcd87e4afee551f5dd8644f1e8a56 (patch)
tree3fe3cc57c587780a1e8278f8c92752f0ff7e2ee0
parent938bad57588976a82c715c3acfdb12e755dd6f3e (diff)
parent0a40f6667af9ff73f8d6b1eb460e050d4ddeb95c (diff)
downloadcliapp-f846d8c4628dcd87e4afee551f5dd8644f1e8a56.tar.gz
Add callbacks to cliapp.runcmd for stdout/err data
-rw-r--r--NEWS7
-rw-r--r--cliapp/runcmd.py23
-rw-r--r--cliapp/runcmd_tests.py35
-rw-r--r--example5.py46
-rw-r--r--without-tests1
5 files changed, 107 insertions, 5 deletions
diff --git a/NEWS b/NEWS
index 8c1a2f6..ca50e4a 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,13 @@
NEWS for cliapp
===============
+Version UNRELEASED
+------------------
+
+* Richard Ipsum added callbacks to `cliapp.runcmd` for handling
+ captured stdout/stderr output from the pipeline. This allows, for
+ example, progress reporting during a long-running command.
+
Version 1.20140719
------------------
diff --git a/cliapp/runcmd.py b/cliapp/runcmd.py
index 6304a48..ee9698e 100644
--- a/cliapp/runcmd.py
+++ b/cliapp/runcmd.py
@@ -84,10 +84,15 @@ def runcmd_unchecked(argv, *argvs, **kwargs):
else:
return default
+ def noop(_):
+ pass
+
feed_stdin = pop_kwarg('feed_stdin', '')
pipe_stdin = pop_kwarg('stdin', subprocess.PIPE)
pipe_stdout = pop_kwarg('stdout', subprocess.PIPE)
pipe_stderr = pop_kwarg('stderr', subprocess.PIPE)
+ stdout_callback = pop_kwarg('stdout_callback', noop)
+ stderr_callback = pop_kwarg('stderr_callback', noop)
try:
pipeline = _build_pipeline(argvs,
@@ -96,7 +101,8 @@ def runcmd_unchecked(argv, *argvs, **kwargs):
pipe_stderr,
kwargs)
return _run_pipeline(pipeline, feed_stdin, pipe_stdin,
- pipe_stdout, pipe_stderr)
+ pipe_stdout, pipe_stderr,
+ stdout_callback, stderr_callback)
except OSError, e: # pragma: no cover
if e.errno == errno.ENOENT and e.filename is None:
e.filename = argv[0]
@@ -129,7 +135,8 @@ def _build_pipeline(argvs, pipe_stdin, pipe_stdout, pipe_stderr, kwargs):
return procs
-def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr):
+def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr,
+ stdout_callback, stderr_callback):
stdout_eof = False
stderr_eof = False
@@ -194,14 +201,22 @@ def _run_pipeline(procs, feed_stdin, pipe_stdin, pipe_stdout, pipe_stderr):
if procs[-1].stdout in r:
data = procs[-1].stdout.read(io_size)
if data:
- out.append(data)
+ logging.debug('calling stdout callback: %r', stdout_callback)
+ data_new = stdout_callback(data)
+ if data_new is None:
+ data_new = data
+ out.append(data_new)
else:
stdout_eof = True
if procs[-1].stderr in r:
data = procs[-1].stderr.read(io_size)
if data:
- err.append(data)
+ logging.debug('calling stderr callback: %r', stderr_callback)
+ data_new = stderr_callback(data)
+ if data_new is None:
+ data_new = data
+ err.append(data_new)
else:
stderr_eof = True
diff --git a/cliapp/runcmd_tests.py b/cliapp/runcmd_tests.py
index e89b73e..3346830 100644
--- a/cliapp/runcmd_tests.py
+++ b/cliapp/runcmd_tests.py
@@ -124,6 +124,40 @@ class RuncmdTests(unittest.TestCase):
self.assertEqual(exit, 0)
self.assertEqual(data, '')
+ def test_runcmd_calls_stdout_callback_when_msg_on_stdout(self):
+ msgs = []
+
+ def logger(s):
+ msgs.append(s)
+
+ # We return a string to allow the callback to mangle
+ # the data being returned.
+ return 'foo'
+
+ test_input = 'hello fox'
+ exit, out, err = cliapp.runcmd_unchecked(['echo', '-n', test_input],
+ stdout_callback=logger)
+
+ self.assertEqual(out, 'foo')
+ self.assertEqual(msgs, [test_input])
+
+ def test_runcmd_calls_stderr_callback_when_msg_on_stderr(self):
+ msgs = []
+
+ def logger(s):
+ msgs.append(s)
+
+ # We return None to signal that the data should not be
+ # mangled.
+ return None
+
+ exit, out, err = cliapp.runcmd_unchecked(['ls', 'nosuchthing'],
+ stderr_callback=logger)
+
+ self.assertEqual(len(msgs), 1)
+ self.assertNotEqual(err, '')
+ self.assertEqual(err, msgs[0])
+
class ShellQuoteTests(unittest.TestCase):
@@ -141,4 +175,3 @@ class ShellQuoteTests(unittest.TestCase):
def test_quotes_single_quote(self):
self.assertEqual(cliapp.shell_quote("'"), '"\'"')
-
diff --git a/example5.py b/example5.py
new file mode 100644
index 0000000..2e83bae
--- /dev/null
+++ b/example5.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2014 Richard Ipsum
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import cliapp
+import logging
+
+
+class LoggerApp(cliapp.Application):
+
+ def log(self, s):
+ ''' A dummy logger function
+
+ In practice this would log the contents
+ to a file or a set of files, perhaps filtering the output or
+ prefixing each line with a date and time.
+
+ '''
+
+ self.output.write('log: %s' % s)
+ self.output.flush()
+
+ def process_args(self, args):
+ commands = [['echo', 'hello world!'], ['ls', 'nosuchthing']]
+
+ for command in commands:
+ try:
+ self.runcmd(command, stdout_callback=self.log,
+ stderr_callback=self.log)
+ except cliapp.AppException:
+ pass
+
+LoggerApp().run()
diff --git a/without-tests b/without-tests
index 9f87e60..c6dc59d 100644
--- a/without-tests
+++ b/without-tests
@@ -10,3 +10,4 @@
./test-plugins/aaa_hello_plugin.py
example3.py
example4.py
+example5.py