diff --git a/libtaskotron/directives/shell_directive.py b/libtaskotron/directives/shell_directive.py
--- a/libtaskotron/directives/shell_directive.py
+++ b/libtaskotron/directives/shell_directive.py
@@ -11,6 +11,7 @@
 from libtaskotron.directives import BaseDirective
 from libtaskotron.exceptions import TaskotronDirectiveError
 from libtaskotron.logger import log
+from libtaskotron import os_utils
 
 DOCUMENTATION = """
 module: shell_directive
@@ -95,22 +96,6 @@
 directive_class = 'ShellDirective'
 
 
-def check_output(cmd, **kwargs):
-    """Custom check_output that prints output in real-time"""
-
-    proc = subprocess.Popen(cmd, **kwargs)
-    output = []
-    for line in iter(proc.stdout.readline, ''):
-        print line,
-        output.append(line)
-
-    retcode = proc.poll()
-    if retcode:
-        raise subprocess.CalledProcessError(retcode, cmd, ''.join(output))
-
-    return ''.join(output)
-
-
 class ShellDirective(BaseDirective):
     def process(self, params, arg_data, ignorereturn=False):
 
@@ -129,12 +114,10 @@
             try:
                 if isinstance(cmd, list) and all([isinstance(c, basestring) for c in cmd]):
                     log.info("Executing process: %s", cmd)
-                    output = check_output(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
-                                          bufsize=1, close_fds=True, cwd=basedir)
+                    output, _ = os_utils.popen_rt(cmd, cwd=basedir)
                 elif isinstance(cmd, basestring):
                     log.info("Executing shell: %s", cmd)
-                    output = check_output(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
-                                          bufsize=1, close_fds=True, cwd=basedir, shell=True)
+                    output, _ = os_utils.popen_rt(cmd, shell=True, cwd=basedir)
                 else:
                     cmd_type = [type(c) for c in cmd] if isinstance(cmd, list) else type(cmd)
                     raise TaskotronDirectiveError("Command %r is in invalid type %r, use string or"
diff --git a/libtaskotron/os_utils.py b/libtaskotron/os_utils.py
--- a/libtaskotron/os_utils.py
+++ b/libtaskotron/os_utils.py
@@ -10,6 +10,7 @@
 import subprocess
 
 from libtaskotron.logger import log
+import libtaskotron.exceptions as exc
 
 
 def is_root():
@@ -44,3 +45,44 @@
     else:
         log.debug(u'✔ Sudo access is available')
         return True
+
+
+def popen_rt(cmd, stderr=subprocess.STDOUT, bufsize=1, **kwargs):
+    """This is similar to :func:`subprocess.check_output`, but with real-time printing to console
+    as well. It is useful for longer-running tasks for which you'd like to both capture the output
+    and also see it printed in terminal continuously.
+
+    Please note that by default both ``stdout`` and ``stderr`` are merged together. You can use
+    ``stderr=subprocess.PIPE``, and it will be returned to you separated, but it won't be printed
+    out to console (only ``stdout`` will).
+
+    The parameters are the same as for :class:`subprocess.Popen`. You can't use ``stdout``
+    parameter, that one is hardcoded to :attr:`subprocess.PIPE`.
+
+    :return: tuple of ``(stdoutdata, stderrdata)``. ``stderrdata`` will be ``None`` by default
+        (because ``stderr`` is redirected to ``stdout``).
+    :raise TaskotronValueError: if you provide ``stdout`` parameter
+    :raise subprocess.CalledProcessError: if the command exits with non-zero exit code (helpful
+        attributes are provided, study its documentation)
+    """
+    if 'stdout' in kwargs:
+        raise exc.TaskotronValueError('stdout parameter not allowed, it will be overridden')
+
+    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, bufsize=bufsize, **kwargs)
+    output = []
+    for line in iter(proc.stdout.readline, ''):
+        print line,
+        output.append(line)
+
+    (stdoutdata, stderrdata) = proc.communicate()
+    assert proc.returncode is not None
+    assert not stdoutdata
+    if stderr == subprocess.STDOUT:
+        assert not stderrdata
+
+    stdoutdata = ''.join(output)
+
+    if proc.returncode:
+        raise subprocess.CalledProcessError(proc.returncode, cmd, stdoutdata)
+
+    return (stdoutdata, stderrdata)
diff --git a/testing/functest_os_utils.py b/testing/functest_os_utils.py
new file mode 100644
--- /dev/null
+++ b/testing/functest_os_utils.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright 2016, Red Hat, Inc.
+# License: GPL-2.0+ <http://spdx.org/licenses/GPL-2.0+>
+# See the LICENSE file for more details on Licensing
+
+'''Functional tests for libtaskotron.os_utils'''
+
+import subprocess
+import pytest
+
+from libtaskotron.os_utils import popen_rt
+
+
+class TestPopenRT(object):
+    '''Test os_utils.popen_rt()'''
+
+    def test_exitcode_zero(self):
+        out, err = popen_rt(['true'])
+        assert not out
+        assert not err
+
+    def test_exitcode_nonzero(self):
+        cmd = ['false']
+        with pytest.raises(subprocess.CalledProcessError) as excinfo:
+            popen_rt(cmd)
+        err = excinfo.value
+        assert err.returncode > 0
+        assert err.cmd == cmd
+        assert not err.output
+
+    def test_stdout(self):
+        text = 'Lorem ipsum'
+        out, err = popen_rt(['echo', '-n', text])
+        assert out == text
+        assert not err
+
+    def test_stderr(self):
+        text = 'Lorem ipsum'
+        out, err = popen_rt('echo -n %s >&2' % text, shell=True, stderr=subprocess.PIPE)
+        assert not out
+        assert err == text
+
+    def test_stream_merge(self):
+        text_out = 'this is stdout'
+        text_err = 'this is stderr'
+        out, err = popen_rt('echo -n %s; echo -n %s >&2' % (text_out, text_err), shell=True)
+        assert out == text_out + text_err
+        assert not err
+
+    def test_stream_separate(self):
+        text_out = 'this is stdout'
+        text_err = 'this is stderr'
+        out, err = popen_rt('echo -n %s; echo -n %s >&2' % (text_out, text_err), shell=True,
+                            stderr=subprocess.PIPE)
+        assert out == text_out
+        assert err == text_err
+
+    def test_shell(self):
+        popen_rt('true && true', shell=True)
+
+    def test_stdin(self, tmpdir):
+        text = 'foo\nbar'
+        infile = tmpdir.join('stdin')
+        infile.write(text)
+        out, err = popen_rt(['cat'], stdin=infile.open())
+        assert out == text
+        assert not err
diff --git a/testing/functest_shell_directive.py b/testing/functest_shell_directive.py
--- a/testing/functest_shell_directive.py
+++ b/testing/functest_shell_directive.py
@@ -9,7 +9,7 @@
 import os
 import pytest
 
-OUTPUT = "Piko a Rybi prsty"
+OUTPUT = "Happy panda"
 PATH = os.path.realpath(__file__)
 
 
diff --git a/testing/test_os_utils.py b/testing/test_os_utils.py
--- a/testing/test_os_utils.py
+++ b/testing/test_os_utils.py
@@ -8,11 +8,13 @@
 import os
 import subprocess
 import mock
+import pytest
 
-from libtaskotron.os_utils import is_root, has_sudo
+from libtaskotron.os_utils import is_root, has_sudo, popen_rt
+import libtaskotron.exceptions as exc
 
 
-class TestIsRoot():
+class TestIsRoot(object):
     '''Test os_utils.is_root()'''
 
     def test_root(self, monkeypatch):
@@ -26,7 +28,7 @@
         assert not is_root()
 
 
-class TestHasSudo():
+class TestHasSudo(object):
     '''Test os_utils.has_sudo()'''
 
     def test_sudo(self, monkeypatch):
@@ -39,3 +41,11 @@
         mock_check_output = mock.Mock(side_effect=err)
         monkeypatch.setattr(subprocess, 'check_output', mock_check_output)
         assert not has_sudo()
+
+
+class TestPopenRT(object):
+    '''Test os_utils.popen_rt()'''
+
+    def test_raise_stdout(self):
+        with pytest.raises(exc.TaskotronValueError):
+            popen_rt(['false'], stdout=None)
diff --git a/testing/test_shell_directive.py b/testing/test_shell_directive.py
--- a/testing/test_shell_directive.py
+++ b/testing/test_shell_directive.py
@@ -5,12 +5,13 @@
 
 from libtaskotron.directives import shell_directive
 from libtaskotron.exceptions import TaskotronDirectiveError
+import libtaskotron.os_utils
 
 import os
 import pytest
 import mock
 
-OUTPUT = "Piko a Rybi prsty"
+OUTPUT = "Happy panda"
 PATH = os.path.realpath(__file__)
 
 
@@ -20,13 +21,13 @@
     @pytest.fixture
     def setup(self, monkeypatch):
         self.shell_dir = shell_directive.ShellDirective()
-        self.mock_check_output = mock.Mock()
-        monkeypatch.setattr(shell_directive, 'check_output', self.mock_check_output)
+        self.mock_run = mock.Mock()
+        monkeypatch.setattr(libtaskotron.os_utils, 'popen_rt', self.mock_run)
         self.arg_data = {'task': PATH}
 
     def test_a_list(self):
         test_params = ['cmd.exe']
-        self.mock_check_output.return_value = OUTPUT
+        self.mock_run.return_value = (OUTPUT, '')
 
         output = self.shell_dir.process(test_params, self.arg_data)
 
@@ -38,20 +39,20 @@
         with pytest.raises(TaskotronDirectiveError):
             self.shell_dir.process(test_params, self.arg_data)
 
-        assert not self.mock_check_output.called
+        assert not self.mock_run.called
 
     def test_cmd_not_a_str(self, monkeypatch):
         test_params = [False]
 
         with pytest.raises(TaskotronDirectiveError):
             self.shell_dir.process(test_params, self.arg_data)
 
-        assert not self.mock_check_output.called
+        assert not self.mock_run.called
 
     def test_cmd_not_a_list_of_strs(self, monkeypatch):
         test_params = [['echo', False]]
 
         with pytest.raises(TaskotronDirectiveError):
             self.shell_dir.process(test_params, self.arg_data)
 
-        assert not self.mock_check_output.called
+        assert not self.mock_run.called

