diff --git a/README.rst b/README.rst
index 599ed6e..c4e5cb4 100644
--- a/README.rst
+++ b/README.rst
@@ -49,7 +49,9 @@ You can do MySQL stuff too::
 If your database url isn't `DATABASE_URL`, you can connect to it by making it
 the first argument::
 
+    $ export FATTYBASE_URL=postgres://fatty@fat/phat
     $ phd FATTYBASE_URL psql
+    psql -U fatty -h fat phat
 
 
 Installation
@@ -60,7 +62,18 @@ Install with pip::
     pip install postdoc
 
 
+Extras
+------
 
+Add the flag `--postdoc-dry-run` to just print the command.
+
+Add the flag `--postdoc-quiet` to execute the command without printing the
+debugging line.
+
+Aliases::
+
+    alias dphd="phd --postdoc-dry-run"
+    alias qphd="phd --postdoc-quiet"
 
 
 
diff --git a/postdoc.py b/postdoc.py
index f1f20fb..4a40e16 100755
--- a/postdoc.py
+++ b/postdoc.py
@@ -1,5 +1,15 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
+"""
+Usage: phd COMMAND [options] [command options]
+
+  COMMAND  A command like psql, createdb, dropdb
+
+Options:
+
+  --postdoc-dry-run  Print output and then exit.
+  --postdoc-quiet    Don't print debugging output.
+"""
 
 import os
 import subprocess
@@ -103,22 +113,18 @@ def get_command(command, meta):
     return bits
 
 
-def main():
-    if '--version' in sys.argv:
-        exit('PostDoc {0}'.format(__version__))
-    if '--help' in sys.argv or len(sys.argv) < 2:
-        exit('Usage: phd COMMAND [additional-options]\n\n'
-            '  ERROR: Must give a COMMAND like psql, createdb, dropdb')
-    # if sys.argv[1] not in VALID_COMMANDS:
+def make_tokens_and_env(sys_argv):
+    """Get the tokens or quit with help."""
+    # if sys_argv[1] not in VALID_COMMANDS:
     #     exit('Usage: phd COMMAND [additional-options]\n\n'
-    #         '  ERROR: "%s" is not a known postgres command' % sys.argv[1])
+    #         '  ERROR: "%s" is not a known postgres command' % sys_argv[1])
 
-    if sys.argv[1].isupper():
-        environ_key = sys.argv[1]
-        args = sys.argv[2:]
+    if sys_argv[1].isupper():
+        environ_key = sys_argv[1]
+        args = sys_argv[2:]
     else:
         environ_key = 'DATABASE_URL'
-        args = sys.argv[1:]
+        args = sys_argv[1:]
 
     try:
         meta = get_uri(environ_key)
@@ -134,16 +140,36 @@ def main():
         env['PGPASSWORD'] = meta.password
     # pass any other flags the user set along
     tokens.extend(args[1:])
-    sys.stderr.write(' '.join(tokens) + '\n')
+    return tokens, env
+
+
+def main():
+    if '--version' in sys.argv:
+        exit('PostDoc {0}'.format(__version__))
+    if '--help' in sys.argv or len(sys.argv) < 2:
+        exit(__doc__)
+    is_dry_run = '--postdoc-dry-run' in sys.argv
+    if is_dry_run:
+        sys.argv.remove('--postdoc-dry-run')
+    is_quiet = '--postdoc-quiet' in sys.argv
+    if is_quiet:
+        sys.argv.remove('--postdoc-quiet')
+
+    tokens, env = make_tokens_and_env(sys.argv)
+    if is_dry_run:
+        sys.stdout.write(' '.join(tokens) + '\n')
+        exit(0)
+    if not is_quiet:
+        sys.stderr.write(' '.join(tokens) + '\n')
     try:
         subprocess.call(tokens, env=env)
     except OSError as e:
         import errno
         if e.errno == errno.ENOENT:  # No such file or directory
-            exit('{0}: command not found'.format(args[0]))
-
+            exit('{0}: command not found'.format(tokens[0]))
     except KeyboardInterrupt:
         pass
 
+
 if __name__ == '__main__':
     main()

Add a way to let the user decide if postdoc outputs anything at all
As a human using postdoc, I want to be able to see what it's doing under the hood. But if I were a script, I may not want any output at all.
diff --git a/test_postdoc.py b/test_postdoc.py
index 0715a83..a61568a 100644
--- a/test_postdoc.py
+++ b/test_postdoc.py
@@ -12,6 +12,12 @@ import mock
 import postdoc
 
 
+# Just a reminder to myself that if I set DATABASE_URL, it will mess up the
+# test suite
+if 'DATABASE_URL' in os.environ:
+    exit('Re-run tests in an environment without DATABASE_URL')
+
+
 class ConnectBitsTest(unittest.TestCase):
     def test_pg_connect_bits_trivial_case(self):
         meta = type('mock', (object, ),
@@ -117,63 +123,106 @@ class PHDTest(unittest.TestCase):
             self.assertEqual(postdoc.get_command('mysql', meta),
                     ['mysql', 'rofl', '--database', 'database'])
 
-    def test_main_exits_with_no_command(self):
-        with mock.patch('postdoc.sys') as mock_sys:
-            mock_sys.argv = ['phd']
+    def test_make_tokens_and_env_exits_with_bad_command(self):
+        with self.assertRaises(SystemExit):
+            postdoc.make_tokens_and_env(['phd', 'fun'])
+
+    def test_make_tokens_and_env_exits_with_missing_env(self):
+        mock_get_command = mock.MagicMock(return_value=['get_command'])
+
+        with mock.patch.multiple(
+            postdoc,
+            get_command=mock_get_command,
+        ):
             with self.assertRaises(SystemExit):
-                postdoc.main()
+                postdoc.make_tokens_and_env(['argv1', 'psql', 'argv3', 'argv4'])
 
-    def test_main_exits_with_bad_command(self):
-        with mock.patch('postdoc.sys') as mock_sys:
-            mock_sys.argv = ['phd', 'fun']
+    def test_make_tokens_and_env_can_use_alternate_url(self):
+        mock_os = mock.MagicMock(environ={
+            'FATTYBASE_URL': 'postgis://u@h/test',
+        })
+
+        with mock.patch.multiple(
+            postdoc,
+            os=mock_os,
+        ):
+            tokens, env = postdoc.make_tokens_and_env(
+                ['argv1', 'FATTYBASE_URL', 'psql', 'extra_arg'])
+            self.assertEqual(tokens,
+                ['psql', '-U', 'u', '-h', 'h', 'test', 'extra_arg'])
+
+    # INTEGRATION TESTING AROUND main() #
+
+    def test_main_exits_with_no_command(self):
+        # TODO verify we exited for the right reason
+        mock_sys = mock.MagicMock(argv=['phd'])
+        with mock.patch.multiple(
+            postdoc,
+            sys=mock_sys,
+        ):
             with self.assertRaises(SystemExit):
                 postdoc.main()
 
-    def test_main_exits_with_missing_env(self):
+    def test_main_works(self):
         mock_subprocess = mock.MagicMock()
+        # to avoid having to patch os.environ
         mock_get_command = mock.MagicMock(return_value=['get_command'])
-        mock_sys = mock.MagicMock()
-        mock_sys.argv = ['argv1', 'psql', 'argv3', 'argv4']
+        mock_get_uri = mock.MagicMock()
+        mock_sys = mock.MagicMock(argv=['phd', 'psql'])
 
         with mock.patch.multiple(
             postdoc,
             subprocess=mock_subprocess,
             get_command=mock_get_command,
+            get_uri=mock_get_uri,
+            sys=mock_sys,
+        ):
+            postdoc.main()
+        self.assertEqual(mock_subprocess.call.call_args[0][0], ['get_command'])
+        self.assertEqual(mock_sys.stderr.write.call_args[0][0], 'get_command\n')
+
+    def test_main_can_do_a_dry_run_to_stdout(self):
+        mock_subprocess = mock.MagicMock()
+        mock_get_command = mock.MagicMock(return_value=['get_command'])
+        mock_get_uri = mock.MagicMock()
+        mock_sys = mock.MagicMock(argv=['phd', 'psql', '--postdoc-dry-run'])
+
+        with mock.patch.multiple(
+            postdoc,
+            subprocess=mock_subprocess,
+            get_command=mock_get_command,
+            get_uri=mock_get_uri,
             sys=mock_sys,
         ):
             with self.assertRaises(SystemExit):
                 postdoc.main()
+        self.assertEqual(mock_sys.stdout.write.call_args[0][0], 'get_command\n')
 
-    def test_main_can_use_alternate_url(self):
+    def test_main_command_debug_can_be_quiet(self):
         mock_subprocess = mock.MagicMock()
-        mock_sys = mock.MagicMock(
-            argv=['argv1', 'FATTYBASE_URL', 'psql', 'extra_arg'],
-        )
-        mock_os = mock.MagicMock(environ={
-            'FATTYBASE_URL': 'postgis://u@h/test',
-        })
+        mock_get_command = mock.MagicMock(return_value=['get_command'])
+        mock_get_uri = mock.MagicMock()
+        mock_sys = mock.MagicMock(argv=['phd', 'psql', '--postdoc-quiet'])
 
         with mock.patch.multiple(
             postdoc,
             subprocess=mock_subprocess,
+            get_command=mock_get_command,
+            get_uri=mock_get_uri,
             sys=mock_sys,
-            os=mock_os,
         ):
             postdoc.main()
-            self.assertEqual(mock_subprocess.call.call_args[0][0],
-                ['psql', '-U', 'u', '-h', 'h', 'test', 'extra_arg'])
+        self.assertEqual(mock_subprocess.call.call_args[0][0], ['get_command'])
+        self.assertFalse(mock_sys.stderr.write.called)
 
     def test_main_passes_password_in_env(self):
         my_password = 'hunter2'
         meta = type('mock', (object, ),
                 {'password': my_password})
-        self.assertNotIn('DATABASE_URL', os.environ,
-            msg="Re-run tests in an environment without DATABASE_URL")
         mock_subprocess = mock.MagicMock()
         mock_get_command = mock.MagicMock(return_value=['get_command'])
         mock_get_uri = mock.MagicMock(return_value=meta)
-        mock_sys = mock.MagicMock()
-        mock_sys.argv = ['foo', 'psql']
+        mock_sys = mock.MagicMock(argv=['foo', 'psql'])
 
         with mock.patch.multiple(
             postdoc,
@@ -188,13 +237,10 @@ class PHDTest(unittest.TestCase):
             my_password)
 
     def test_main_appends_additional_flags(self):
-        self.assertNotIn('DATABASE_URL', os.environ,
-            msg="Re-run tests in an environment without DATABASE_URL")
         mock_subprocess = mock.MagicMock()
         mock_get_command = mock.MagicMock(return_value=['get_command'])
         mock_get_uri = mock.MagicMock()
-        mock_sys = mock.MagicMock()
-        mock_sys.argv = ['argv1', 'psql', 'argv3', 'argv4']
+        mock_sys = mock.MagicMock(argv=['argv1', 'psql', 'argv3', 'argv4'])
 
         with mock.patch.multiple(
             postdoc,
@@ -204,17 +250,16 @@ class PHDTest(unittest.TestCase):
             sys=mock_sys,
         ):
             postdoc.main()
-            self.assertEqual(
-                mock_subprocess.call.call_args[0][0],
-                ['get_command', 'argv3', 'argv4']
-            )
+        self.assertEqual(
+            mock_subprocess.call.call_args[0][0],
+            ['get_command', 'argv3', 'argv4']
+        )
 
     def test_nonsense_command_has_meaningful_error(self):
         mock_os = mock.MagicMock(environ={
             'DATABASE_URL': 'postgis://u@h/test',
         })
-        mock_sys = mock.MagicMock(
-            argv=['phd', 'xyzzy'])
+        mock_sys = mock.MagicMock(argv=['phd', 'xyzzy'])
         with mock.patch.multiple(
             postdoc,
             os=mock_os,

pip install -e .
parse_log_pytest
pytest
pytest mock
find . -name '*.pyc' -delete find . -name '.DS_Store' -delete rm -rf *.egg rm -rf *.egg-info rm -rf __pycache__ rm -rf build rm -rf dist
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
mock==5.2.0
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
-e git+https://github.com/crccheck/postdoc.git@4080450041ae86b295a2e1f8aea2018bf5e4b32a#egg=postdoc
pytest @ file:///croot/pytest_1738938843180/work
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: postdoc
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
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_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
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=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
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - mock==5.2.0
prefix: /opt/conda/envs/postdoc

test_postdoc.py::PHDTest::test_main_can_do_a_dry_run_to_stdout test_postdoc.py::PHDTest::test_main_command_debug_can_be_quiet test_postdoc.py::PHDTest::test_make_tokens_and_env_can_use_alternate_url test_postdoc.py::PHDTest::test_make_tokens_and_env_exits_with_bad_command test_postdoc.py::PHDTest::test_make_tokens_and_env_exits_with_missing_env
test_postdoc.py::ConnectBitsTest::test_connect_bits_supported_schemas test_postdoc.py::ConnectBitsTest::test_mysql_connect_bits_trivial_case test_postdoc.py::ConnectBitsTest::test_mysql_connect_bits_works test_postdoc.py::ConnectBitsTest::test_pg_connect_bits_trivial_case test_postdoc.py::ConnectBitsTest::test_pg_connect_bits_works test_postdoc.py::PHDTest::test_get_command_assembles_bits_in_right_order test_postdoc.py::PHDTest::test_get_command_ignores_password test_postdoc.py::PHDTest::test_get_command_special_syntax_for_mysql test_postdoc.py::PHDTest::test_get_command_special_syntax_for_pg_restore test_postdoc.py::PHDTest::test_get_commands_can_ignore_database_name test_postdoc.py::PHDTest::test_get_uri test_postdoc.py::PHDTest::test_main_appends_additional_flags test_postdoc.py::PHDTest::test_main_exits_with_no_command test_postdoc.py::PHDTest::test_main_passes_password_in_env test_postdoc.py::PHDTest::test_main_works test_postdoc.py::PHDTest::test_nonsense_command_has_meaningful_error
README.rst postdoc.py
README.rst postdoc.py