diff --git a/watch_do/cli.py b/watch_do/cli.py
index 2a5474b..7a70bfc 100644
--- a/watch_do/cli.py
+++ b/watch_do/cli.py
@@ -230,7 +230,7 @@ def watch_do():
                 start_time = time.time()
                 for file_name in changed_files:
                     for output in doer_manager.run_doers(file_name):
-                        print(output)
+                        print(output, end='')
 
                     if not args.multi:
                         break
diff --git a/watch_do/doer_manager.py b/watch_do/doer_manager.py
index 1ed7ab8..a11c347 100644
--- a/watch_do/doer_manager.py
+++ b/watch_do/doer_manager.py
@@ -117,14 +117,11 @@ class DoerManager:
         return doers
 
     def run_doers(self, file_name):
-        """Run each doer in turn and return their output.
+        """Run each doer in turn and yield its output.
 
-        Returns:
-            list: A list of strings that contain the combined output of stdout
-            and stderr from the doers.
+        Yields:
+            str: A string that contains the combined output of stdout and
+                stderr from the doers.
         """
-        results = []
         for doer in self.doers:
-            results.append(doer.run(file_name))
-
-        return results
+            yield from doer.run(file_name)
diff --git a/watch_do/doers/doer.py b/watch_do/doers/doer.py
index aa14900..952db28 100644
--- a/watch_do/doers/doer.py
+++ b/watch_do/doers/doer.py
@@ -85,7 +85,8 @@ class Doer(metaclass=ABCMeta):
         Parameters:
             file_name (str): The file name to run this doer against.
 
-        Returns:
-            str: A string, containing the status/output of the action.
+        Yields:
+            str: A string containing the output (possibly the partial output)
+                of the command, both stdout and stderr.
         """
         pass
diff --git a/watch_do/doers/shell.py b/watch_do/doers/shell.py
index e12c6e4..fcc8364 100644
--- a/watch_do/doers/shell.py
+++ b/watch_do/doers/shell.py
@@ -35,20 +35,21 @@ class Shell(Doer):
             file_name (str): The ``file_name`` that this doer should run
                 against.
 
-        Returns:
-            str: A string containing the output of the command, both stdout and
-                 stderr.
+        Yields:
+            str: A string containing the output (possibly the partial output)
+                of the command, both stdout and stderr.
         """
         command = Doer._interpolate_file_name(self.command, file_name)
 
-        try:
-            output = subprocess.check_output(
-                command, stderr=subprocess.STDOUT, shell=True)
-        except subprocess.CalledProcessError as ex:
-            output = (
-                ex.output +
-                b'\nCommand failed to run, exited with error code ' +
-                bytes(str(ex.returncode), 'utf-8')
-            )
-
-        return output.decode('utf-8')
+        with subprocess.Popen(command, stdout=subprocess.PIPE,
+                              stderr=subprocess.STDOUT, bufsize=1,
+                              shell=True) as process:
+            for line in process.stdout:
+                yield line.decode('UTF-8')
+
+            process.wait()
+
+            # If the command returned a non 0 exit code, yield an error message
+            if process.returncode > 0:
+                yield ('Command failed to run, exited with error code {}'
+                       .format(process.returncode))

Incremental output
When a doer takes more than a few seconds to run and is incrementally outputting data, it would be good to reflect this in the output of Watch Do, rather than buffering the whole output and then outputting it all at the end.

An example would be running the following command:
`watch-do -w file -d 'for i in {1..10} :; do date; sleep 1; done'`

This will take over 10 seconds before _any_ output is displayed to the terminal.
diff --git a/tests/doer_manager.py b/tests/doer_manager.py
index 015c4b2..e024d71 100644
--- a/tests/doer_manager.py
+++ b/tests/doer_manager.py
@@ -45,9 +45,9 @@ class TestDoerManager(TestCase):
         """Check that the doers are being run successfully.
         """
         self.assertEqual(
-            self.doer_manager.run_doers('/some/random/file'),
+            list(self.doer_manager.run_doers('/some/random/file')),
             [
                 '/some/random/file has changed.\n',
-                '\nCommand failed to run, exited with error code 1',
+                'Command failed to run, exited with error code 1',
                 'Bye\n'
             ])
diff --git a/tests/doers/shell.py b/tests/doers/shell.py
index 8d7152d..317c54f 100644
--- a/tests/doers/shell.py
+++ b/tests/doers/shell.py
@@ -21,15 +21,15 @@ class TestShell(TestCase):
         """
         shell = Shell('echo -n "This file changed: %f"')
         self.assertEqual(
-            shell.run('/some/random/file'),
-            'This file changed: /some/random/file')
+            list(shell.run('/some/random/file')),
+            ['This file changed: /some/random/file'])
 
         shell = Shell('echo -n "Hello "; echo -n "World" >&2; echo -n "...";')
         self.assertEqual(
-            shell.run(''),
-            'Hello World...')
+            list(shell.run('')),
+            ['Hello World...'])
 
         shell = Shell('echo -n "Hello"; exit 1')
         self.assertEqual(
-            shell.run(''),
-            'Hello\nCommand failed to run, exited with error code 1')
+            list(shell.run('')),
+            ['Hello', 'Command failed to run, exited with error code 1'])

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
astroid==1.4.9
babel==2.17.0
bumpversion==0.5.3
certifi==2025.1.31
charset-normalizer==3.4.1
coverage==7.8.0
docutils==0.21.2
exceptiongroup==1.2.2
idna==3.10
imagesize==1.4.1
iniconfig==2.1.0
isort==6.0.1
Jinja2==3.1.6
lazy-object-proxy==1.10.0
MarkupSafe==3.0.2
mccabe==0.7.0
packaging==24.2
pluggy==1.5.0
Pygments==2.19.1
pylint==1.6.5
pytest==8.3.5
pytest-cov==6.0.0
requests==2.32.3
six==1.17.0
snowballstemmer==2.2.0
Sphinx==1.5.2
tomli==2.2.1
urllib3==2.3.0
-e git+https://github.com/vimist/watch-do.git@909d01ff409b9a7a107f7b563e01f6084e358a71#egg=watch_do
wrapt==1.17.2

name: watch-do
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - astroid==1.4.9
      - babel==2.17.0
      - bumpversion==0.5.3
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - idna==3.10
      - imagesize==1.4.1
      - iniconfig==2.1.0
      - isort==6.0.1
      - jinja2==3.1.6
      - lazy-object-proxy==1.10.0
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - packaging==24.2
      - pluggy==1.5.0
      - pygments==2.19.1
      - pylint==1.6.5
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - requests==2.32.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==1.5.2
      - tomli==2.2.1
      - urllib3==2.3.0
      - wrapt==1.17.2
prefix: /opt/conda/envs/watch-do

tests/doer_manager.py::TestDoerManager::test_run_doers tests/doers/shell.py::TestShell::test_run
tests/doer_manager.py::TestDoerManager::test___init__ tests/doer_manager.py::TestDoerManager::test__process_commands tests/doers/shell.py::TestShell::test__init__
watch_do/doers/shell.py watch_do/doer_manager.py watch_do/cli.py watch_do/doers/doer.py
watch_do/doers/shell.py watch_do/doer_manager.py watch_do/cli.py watch_do/doers/doer.py