Repository: srsudar/eg

Issue:
Recursive custom-dir
Hi,

I would like to classify my md files in directories like:
```
~/eg
~/eg/glossary
~/eg/tech
```

and in my config file, just having something like:
```
custom-dir=~/eg
```

Actually, when a md file is in a subdirectory, it is not found. Is there a way to change that ?

Thanks.

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 04df878..4d76ba6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ syntax: glob
 *.egg-info
 dist
 build
+
+.pytest_cache/
diff --git a/.travis.yml b/.travis.yml
index 5ae51e4..b04d5f9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,4 +7,4 @@ python:
   - "3.4"
 
 # Command to run tests
-script: nosetests
+script: py.test
diff --git a/README.md b/README.md
index a630ec7..fe0fa0f 100644
--- a/README.md
+++ b/README.md
@@ -49,9 +49,8 @@ change, but you'll have to update your links to point at the new location. Or
 you can install with `pip`.
 
 `eg` doesn't ship with a binary. Dependencies are very modest and should not
-require you to install anything (other than
-[Nose](https://github.com/nose-devs/nose/) if you want to run the tests).
-If you find otherwise, open an issue.
+require you to install anything (other than [pytest](https://docs.pytest.org) if
+you want to run the tests).  If you find otherwise, open an issue.
 
 
 ## Usage
@@ -95,10 +94,10 @@ like everybody else? You can create an example for untarring and unzipping
 bzipped tarballs, stick it in a file called `tar.md`, and tell `eg` where to
 find it.
 
-The way to think about what `eg` does is just that it takes a program name, for
-example `find`, and looks for two files named `find.md` in the default
-directory and a custom directory. If it finds them, it pipes them through
-`less`, with the custom file at the top. Easy.
+The way to think about what `eg` does is that it takes a program name, for
+example `find`, and looks for files named `find.md` in the default and custom
+directories (including subdirectories). If it finds them, it pipes them through
+`less`, with the custom files at the top. Easy.
 
 The default and custom directories can be specified at the command line like
 so:
@@ -353,8 +352,9 @@ available.
 `eg` depends only on standard libraries and Python 2.x, so building should be a
 simple matter of cloning the repo and running the executable `eg/eg.py`.
 
-`eg` uses Nose for testing, so you'll have to have Nose installed to run tests.
-Once you have Nose, run `nosetests` from **the root directory of the repo**.
+`eg` uses pytest for testing, so you'll have to have Nose installed to run
+tests.  Once you have Nose, run `py.test` from **the root directory of the
+repo**.
 
 Tests should always be expected to pass. If they fail, please open an issue,
 even if only so that we can better elucidate `eg`'s dependencies.
diff --git a/eg/util.py b/eg/util.py
index 56a2637..9ccae04 100644
--- a/eg/util.py
+++ b/eg/util.py
@@ -46,6 +46,24 @@ def _inform_cannot_edit_no_custom_dir():
     print(msg)
 
 
+def _recursive_get_all_file_names(dir):
+    """
+    Get all the file names in the directory. Gets all the top level file names
+    only, not the full path.
+
+    dir: a directory or string, as to hand to os.walk(). If None, returns empty
+        list.
+    """
+    if not dir:
+        return []
+
+    result = []
+    for basedir, dirs, files in os.walk(dir):
+        result.extend(files)
+
+    return result
+
+
 def edit_custom_examples(program, config):
     """
     Edit custom examples for the given program, creating the file if it does
@@ -57,34 +75,37 @@ def edit_custom_examples(program, config):
 
     # resolve aliases
     resolved_program = get_resolved_program(program, config)
-    custom_file_path = get_file_path_for_program(
+    custom_file_paths = get_file_paths_for_program(
         resolved_program,
         config.custom_dir
     )
-    subprocess.call([config.editor_cmd, custom_file_path])
 
+    if (len(custom_file_paths) > 0):
+        path_to_edit = custom_file_paths[0]
+    else:
+        # A new file.
+        path_to_edit = os.path.join(config.custom_dir, resolved_program + '.md')
 
-def handle_program(program, config):
-    default_file_path = None
-    custom_file_path = None
+    # Edit the first. Handles the base case.
+    subprocess.call([config.editor_cmd, path_to_edit])
 
+
+def handle_program(program, config):
     # try to resolve any aliases
     resolved_program = get_resolved_program(program, config)
 
-    if has_default_entry_for_program(resolved_program, config):
-        default_file_path = get_file_path_for_program(
-            resolved_program,
-            config.examples_dir
-        )
+    default_file_paths = get_file_paths_for_program(
+        resolved_program,
+        config.examples_dir
+    )
 
-    if has_custom_entry_for_program(resolved_program, config):
-        custom_file_path = get_file_path_for_program(
-            resolved_program,
-            config.custom_dir
-        )
+    custom_file_paths = get_file_paths_for_program(
+        resolved_program,
+        config.custom_dir
+    )
 
     # Handle the case where we have nothing for them.
-    if default_file_path is None and custom_file_path is None:
+    if len(default_file_paths) == 0 and len(custom_file_paths) == 0:
         print(
             'No entry found for ' +
             program +
@@ -92,10 +113,9 @@ def handle_program(program, config):
         )
         return
 
-    raw_contents = get_contents_from_files(
-        default_file_path,
-        custom_file_path
-    )
+    paths = [path for path in custom_file_paths]
+    paths.extend(default_file_paths)
+    raw_contents = get_contents_from_files(*paths)
 
     formatted_contents = get_formatted_contents(
         raw_contents,
@@ -108,48 +128,29 @@ def handle_program(program, config):
     page_string(formatted_contents, config.pager_cmd)
 
 
-def get_file_path_for_program(program, dir_to_search):
+def get_file_paths_for_program(program, dir_to_search):
     """
-    Return the file name and path for the program.
-
-    examples_dir cannot be None
+    Return an array of full paths matching the given program. If no directory is
+    present, returns an empty list.
 
     Path is not guaranteed to exist. Just says where it should be if it
     existed. Paths must be fully expanded before being passed in (i.e. no ~ or
     variables).
     """
     if dir_to_search is None:
-        raise TypeError('examples_dir cannot be None')
+        return []
     else:
-        result = os.path.join(dir_to_search, program + EXAMPLE_FILE_SUFFIX)
-        return result
-
+        wanted_file_name = program + EXAMPLE_FILE_SUFFIX
+        result = []
+        for basedir, dirs, file_names in os.walk(dir_to_search):
+            for file_name in file_names:
+                if file_name == wanted_file_name:
+                    result.append(os.path.join(basedir, file_name))
 
-def has_default_entry_for_program(program, config):
-    """Return True if has standard examples for program, else False."""
-    if config.examples_dir:
-        file_path = get_file_path_for_program(
-            program,
-            config.examples_dir
-        )
-        return os.path.isfile(file_path)
-    else:
-        return False
-
-
-def has_custom_entry_for_program(program, config):
-    """Return True if has custom examples for a program, else false."""
-    if config.custom_dir:
-        custom_path = get_file_path_for_program(
-            program,
-            config.custom_dir
-        )
-        return os.path.isfile(custom_path)
-    else:
-        return False
+        return result
 
 
-def get_contents_from_files(default_file_path, custom_file_path):
+def get_contents_from_files(*paths):
     """
     Take the paths to two files and return the contents as a string. If
     custom_file_path is valid, it will be shown before the contents of the
@@ -157,11 +158,8 @@ def get_contents_from_files(default_file_path, custom_file_path):
     """
     file_data = ''
 
-    if custom_file_path:
-        file_data += _get_contents_of_file(custom_file_path)
-
-    if default_file_path:
-        file_data += _get_contents_of_file(default_file_path)
+    for path in paths:
+        file_data += _get_contents_of_file(path)
 
     return file_data
 
@@ -224,18 +222,14 @@ def get_list_of_all_supported_commands(config):
     custom file names. This is intentional, as that is the behavior for file
     resolution--an alias will hide a custom file.
     """
-    default_files = []
-    custom_files = []
-
-    if config.examples_dir and os.path.isdir(config.examples_dir):
-        default_files = os.listdir(config.examples_dir)
-    if config.custom_dir and os.path.isdir(config.custom_dir):
-        custom_files = os.listdir(config.custom_dir)
+    default_files = _recursive_get_all_file_names(config.examples_dir)
+    custom_files = _recursive_get_all_file_names(config.custom_dir)
 
     # Now filter so we only have example files, not things like aliases.json.
     default_files = [path for path in default_files if _is_example_file(path)]
     custom_files = [path for path in custom_files if _is_example_file(path)]
 
+
     def get_without_suffix(file_name):
         """
         Return the file name without the suffix, or the file name itself
diff --git a/setup.py b/setup.py
index df9d3c3..926d174 100644
--- a/setup.py
+++ b/setup.py
@@ -40,14 +40,13 @@ config = {
     'author_email': 'sudar.sam@gmail.com',
     'version': VERSION,
     'install_requires': [],
-    'test_requires': ['nose', 'mock'],
+    'test_requires': ['mock', 'pytest'],
     'packages': ['eg'],
     'scripts': ['bin/eg'],
     'package_data': {
         'eg': ['examples/*']
     },
     'zip_safe': False,
-    'test_suite': 'nose.collector'
 }
 
 setup(**config)


Test patch:
diff --git a/test/color_test.py b/test/color_test.py
index df64352..985b712 100644
--- a/test/color_test.py
+++ b/test/color_test.py
@@ -4,7 +4,6 @@ from collections import namedtuple
 from eg import color
 from eg import config
 from mock import patch
-from nose.tools import assert_equal
 
 # Some hardcoded real colors.
 _YELLOW = '\x1b[33m'
@@ -138,8 +137,7 @@ def test_colorize_heading():
     colorizer = color.EgColorizer(color_config)
 
     actual = colorizer.colorize_heading(clean)
-
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_colorize_block_indents():
@@ -171,8 +169,7 @@ def test_colorize_block_indents():
     colorizer = color.EgColorizer(color_config)
 
     actual = colorizer.colorize_block_indent(clean)
-
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_colorize_backticks():
@@ -202,28 +199,19 @@ def test_colorize_backticks():
     colorizer = color.EgColorizer(color_config)
 
     actual = colorizer.colorize_backticks(clean)
-
-    assert_equal(actual, target)
-
-
-def test_colorize_text_calls_all_sub_methods():
+    assert actual == target
+
+@patch('eg.color.EgColorizer.colorize_backticks',
+       return_value='text-heading-indent-backticks')
+@patch('eg.color.EgColorizer.colorize_block_indent',
+       return_value='text-heading-indent')
+@patch('eg.color.EgColorizer.colorize_heading', return_value='text-heading')
+def test_colorize_text_calls_all_sub_methods(heading, indent, backticks):
     """colorize_text should call all of the helper colorize methods."""
-    with patch(
-        'eg.color.EgColorizer.colorize_heading',
-        return_value='text-heading'
-    ) as heading:
-        with patch(
-            'eg.color.EgColorizer.colorize_block_indent',
-            return_value='text-heading-indent'
-        ) as indent:
-            with patch(
-                'eg.color.EgColorizer.colorize_backticks',
-                return_value='text-heading-indent-backticks'
-            ) as backticks:
-                colorizer = color.EgColorizer(None)
-                text = 'text'
-                actual = colorizer.colorize_text(text)
-                heading.assert_called_once_with(text)
-                indent.assert_called_once_with('text-heading')
-                backticks.assert_called_once_with('text-heading-indent')
-                assert_equal('text-heading-indent-backticks', actual)
+    colorizer = color.EgColorizer(None)
+    text = 'text'
+    actual = colorizer.colorize_text(text)
+    heading.assert_called_once_with(text)
+    indent.assert_called_once_with('text-heading')
+    backticks.assert_called_once_with('text-heading-indent')
+    assert 'text-heading-indent-backticks' == actual
diff --git a/test/config_test.py b/test/config_test.py
index 1383dc3..7c06219 100644
--- a/test/config_test.py
+++ b/test/config_test.py
@@ -1,12 +1,9 @@
 import os
+import pytest
 
 from eg import config
 from eg import substitute
 from mock import patch
-from nose.tools import assert_equal
-from nose.tools import assert_false
-from nose.tools import assert_raises
-from nose.tools import assert_true
 
 # Support python 2 and 3
 try:
@@ -96,14 +93,14 @@ def test_config_returns_egrc_values_if_present(mock_get_config, mock_isfile):
         None,
     )
 
-    assert_equal(resolved_config.examples_dir, examples_dir)
-    assert_equal(resolved_config.custom_dir, custom_dir)
-    assert_equal(resolved_config.color_config, test_color_config)
-    assert_equal(resolved_config.use_color, test_use_color)
-    assert_equal(resolved_config.pager_cmd, test_pager_cmd)
-    assert_equal(resolved_config.editor_cmd, test_editor_cmd)
-    assert_equal(resolved_config.squeeze, test_squeeze)
-    assert_equal(resolved_config.subs, test_subs)
+    assert resolved_config.examples_dir == examples_dir
+    assert resolved_config.custom_dir == custom_dir
+    assert resolved_config.color_config == test_color_config
+    assert resolved_config.use_color == test_use_color
+    assert resolved_config.pager_cmd == test_pager_cmd
+    assert resolved_config.editor_cmd == test_editor_cmd
+    assert resolved_config.squeeze == test_squeeze
+    assert resolved_config.subs == test_subs
 
 
 def _call_get_resolved_config_with_defaults(
@@ -137,7 +134,7 @@ def test_inform_if_paths_invalid_selectively_informs(mock_inform):
     """
     config.inform_if_paths_invalid(None, None, None)
 
-    assert_equal(mock_inform.call_count, 0)
+    assert mock_inform.call_count == 0
 
     egrc_path = 'egrc'
     ex_dir = 'ex dir'
@@ -145,7 +142,7 @@ def test_inform_if_paths_invalid_selectively_informs(mock_inform):
 
     config.inform_if_paths_invalid(egrc_path, ex_dir, cu_dir)
 
-    assert_equal(mock_inform.call_count, 3)
+    assert mock_inform.call_count == 3
     mock_inform.assert_any_call(egrc_path)
     mock_inform.assert_any_call(ex_dir)
     mock_inform.assert_any_call(cu_dir)
@@ -229,7 +226,7 @@ def _assert_about_get_egrc_config(
 
     actual = config.get_egrc_config(cli_path)
 
-    assert_equal(actual, expected_config)
+    assert actual == expected_config
 
     mock_expand.assert_called_once_with(path_to_expand)
     mock_isfile.assert_called_once_with(expanded_path)
@@ -263,8 +260,8 @@ def test_get_resolved_config_calls_expand_paths(
 
     actual = _call_get_resolved_config_with_defaults()
 
-    assert_equal(actual.examples_dir, expected_examples_dir)
-    assert_equal(actual.custom_dir, expected_custom_dir)
+    assert actual.examples_dir == expected_examples_dir
+    assert actual.custom_dir == expected_custom_dir
 
 
 @patch('eg.config.get_editor_cmd_from_environment')
@@ -300,14 +297,14 @@ def _assert_about_get_resolved_config(
         debug=False
     )
 
-    assert_equal(actual.examples_dir, expected_config.examples_dir)
-    assert_equal(actual.custom_dir, expected_config.custom_dir)
-    assert_equal(actual.use_color, expected_config.use_color)
-    assert_equal(actual.color_config, expected_config.color_config)
-    assert_equal(actual.pager_cmd, expected_config.pager_cmd)
-    assert_equal(actual.squeeze, expected_config.squeeze)
-    assert_equal(actual.subs, expected_config.subs)
-    assert_equal(actual.editor_cmd, expected_config.editor_cmd)
+    assert actual.examples_dir == expected_config.examples_dir
+    assert actual.custom_dir == expected_config.custom_dir
+    assert actual.use_color == expected_config.use_color
+    assert actual.color_config == expected_config.color_config
+    assert actual.pager_cmd == expected_config.pager_cmd
+    assert actual.squeeze == expected_config.squeeze
+    assert actual.subs == expected_config.subs
+    assert actual.editor_cmd == expected_config.editor_cmd
 
     mock_get_egrc_config.assert_called_once_with(cli_egrc_path)
     mock_get_editor.assert_called_once_with()
@@ -413,10 +410,11 @@ def test_get_config_tuple_from_egrc_all_none_when_not_present():
         subs=None,
         editor_cmd=None,
     )
-    assert_equal(actual, target)
+    assert actual == target
 
 
-def test_get_config_tuple_from_egrc_when_present():
+@patch('eg.config.get_expanded_path')
+def test_get_config_tuple_from_egrc_when_present(mock_expand):
     """
     Make sure we extract values correctly from the egrc.
     """
@@ -448,28 +446,25 @@ def test_get_config_tuple_from_egrc_when_present():
                 egrc_custom_dir
             )
 
-    with patch(
-        'eg.config.get_expanded_path',
-        side_effect=return_expanded_path
-    ) as mock_expand:
+    mock_expand.side_effect = return_expanded_path
 
-        actual = config.get_config_tuple_from_egrc(PATH_EGRC_WITH_DATA)
+    actual = config.get_config_tuple_from_egrc(PATH_EGRC_WITH_DATA)
 
-        expected = config.Config(
-            examples_dir=egrc_examples_dir,
-            custom_dir=egrc_custom_dir,
-            color_config=color_config_from_file,
-            use_color=egrc_use_color,
-            pager_cmd=egrc_pager_cmd,
-            squeeze=egrc_squeeze,
-            subs=egrc_subs,
-            editor_cmd=egrc_editor_cmd,
-        )
+    expected = config.Config(
+        examples_dir=egrc_examples_dir,
+        custom_dir=egrc_custom_dir,
+        color_config=color_config_from_file,
+        use_color=egrc_use_color,
+        pager_cmd=egrc_pager_cmd,
+        squeeze=egrc_squeeze,
+        subs=egrc_subs,
+        editor_cmd=egrc_editor_cmd,
+    )
 
-        assert_equal(actual, expected)
+    assert actual == expected
 
-        mock_expand.assert_any_call(egrc_examples_dir)
-        mock_expand.assert_any_call(egrc_custom_dir)
+    mock_expand.assert_any_call(egrc_examples_dir)
+    mock_expand.assert_any_call(egrc_custom_dir)
 
 
 def _get_color_config_from_egrc_withdata():
@@ -507,7 +502,7 @@ def test_merge_color_configs_first_all_none():
 
     merged = config.merge_color_configs(first, second)
 
-    assert_equal(merged, second)
+    assert merged == second
 
 
 def test_merge_color_configs_take_all_first():
@@ -528,7 +523,7 @@ def test_merge_color_configs_take_all_first():
 
     merged = config.merge_color_configs(first, second)
 
-    assert_equal(merged, first)
+    assert merged == first
 
 
 def test_merge_color_configs_mixed():
@@ -562,62 +557,59 @@ def test_merge_color_configs_mixed():
         prompt_reset=second.prompt_reset
     )
 
-    assert_equal(merged, target)
+    assert merged == target
 
 
 def test_default_color_config():
     """Make sure the default color config is set to the right values."""
     actual = config.get_default_color_config()
 
-    assert_equal(actual.pound, config.DEFAULT_COLOR_POUND)
-    assert_equal(actual.heading, config.DEFAULT_COLOR_HEADING)
-    assert_equal(actual.code, config.DEFAULT_COLOR_CODE)
-    assert_equal(actual.backticks, config.DEFAULT_COLOR_BACKTICKS)
-    assert_equal(actual.prompt, config.DEFAULT_COLOR_PROMPT)
-
-    assert_equal(actual.pound_reset, config.DEFAULT_COLOR_POUND_RESET)
-    assert_equal(actual.heading_reset, config.DEFAULT_COLOR_HEADING_RESET)
-    assert_equal(actual.code_reset, config.DEFAULT_COLOR_CODE_RESET)
-    assert_equal(
-        actual.backticks_reset,
-        config.DEFAULT_COLOR_BACKTICKS_RESET
-    )
-    assert_equal(actual.prompt_reset, config.DEFAULT_COLOR_PROMPT_RESET)
+    assert actual.pound == config.DEFAULT_COLOR_POUND
+    assert actual.heading == config.DEFAULT_COLOR_HEADING
+    assert actual.code == config.DEFAULT_COLOR_CODE
+    assert actual.backticks == config.DEFAULT_COLOR_BACKTICKS
+    assert actual.prompt == config.DEFAULT_COLOR_PROMPT
+
+    assert actual.pound_reset == config.DEFAULT_COLOR_POUND_RESET
+    assert actual.heading_reset == config.DEFAULT_COLOR_HEADING_RESET
+    assert actual.code_reset == config.DEFAULT_COLOR_CODE_RESET
+    assert actual.backticks_reset == config.DEFAULT_COLOR_BACKTICKS_RESET
+    assert actual.prompt_reset == config.DEFAULT_COLOR_PROMPT_RESET
 
 
 def test_parse_bool_true_for_truthy_values():
     """We should parse both 'True' and 'true' to True."""
-    assert_true(config._parse_bool_from_raw_egrc_value('True'))
-    assert_true(config._parse_bool_from_raw_egrc_value('true'))
+    assert config._parse_bool_from_raw_egrc_value('True') == True
+    assert config._parse_bool_from_raw_egrc_value('true') == True
 
 
 def test_parse_bool_false_for_non_truthy_values():
     """Make sure we parse the likely non-truthy things as false."""
-    assert_false(config._parse_bool_from_raw_egrc_value(''))
-    assert_false(config._parse_bool_from_raw_egrc_value(None))
-    assert_false(config._parse_bool_from_raw_egrc_value('false'))
-    assert_false(config._parse_bool_from_raw_egrc_value('False'))
+    assert config._parse_bool_from_raw_egrc_value('') == False
+    assert config._parse_bool_from_raw_egrc_value(None) == False
+    assert config._parse_bool_from_raw_egrc_value('false') == False
+    assert config._parse_bool_from_raw_egrc_value('False') == False
 
 
 def test_get_priority_first():
     """The first non-None value should always be returned."""
     target = 'alpha'
     actual = config.get_priority(target, 'second', 'third')
-    assert_equal(target, actual)
+    assert target == actual
 
 
 def test_get_priority_second():
     """The second non-None should be returned if the first is None."""
     target = 'beta'
     actual = config.get_priority(None, target, 'third')
-    assert_equal(target, actual)
+    assert target == actual
 
 
 def test_get_priority_third():
     """The last should be taken if the first two are None."""
     target = 'gamma'
     actual = config.get_priority(None, None, target)
-    assert_equal(target, actual)
+    assert target == actual
 
 
 def test_get_priority_respect_false():
@@ -628,7 +620,7 @@ def test_get_priority_respect_false():
     """
     target = False
     actual = config.get_priority(False, 'second', 'third')
-    assert_equal(target, actual)
+    assert target == actual
 
 
 def test_parse_substitution_from_list_without_is_multiline():
@@ -639,7 +631,7 @@ def test_parse_substitution_from_list_without_is_multiline():
     target = substitute.Substitution('foo', 'bar', False)
     list_rep = ['foo', 'bar']
     actual = config.parse_substitution_from_list(list_rep)
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_parse_substitution_from_list_with_is_multiline():
@@ -649,36 +641,32 @@ def test_parse_substitution_from_list_with_is_multiline():
     target = substitute.Substitution('patt', 'repl', True)
     list_rep = ['patt', 'repl', True]
     actual = config.parse_substitution_from_list(list_rep)
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_parse_substitution_error_if_not_list():
     """
     Raise a SyntaxError if the value is not a list.
     """
-    assert_raises(SyntaxError, config.parse_substitution_from_list, 'foo_str')
+    with pytest.raises(SyntaxError):
+        config.parse_substitution_from_list('foo_str')
 
 
 def test_parse_substitution_error_if_wrong_length():
     """
     Raise a SyntaxError if the list is less than two long.
     """
-    assert_raises(
-        SyntaxError,
-        config.parse_substitution_from_list,
-        ['foo']
-    )
+    with pytest.raises(SyntaxError):
+        config.parse_substitution_from_list(['foo'])
 
 
 def test_parse_substitution_error_if_third_element_not_bool():
     """
     Raise a SyntaxError if the third element in the list is not a boolean.
     """
-    assert_raises(
-        SyntaxError,
-        config.parse_substitution_from_list,
-        ['foo', 'bar', 'intentionally_not_a_bool']
-    )
+    with pytest.raises(SyntaxError):
+        bad_args = ['foo', 'bar', 'intentionally_not_a_bool']
+        config.parse_substitution_from_list(bad_args)
 
 
 def test_get_substitution_from_config_finds_single_substitution():
@@ -693,7 +681,7 @@ def test_get_substitution_from_config_finds_single_substitution():
     config_obj = _get_egrc_config(PATH_EGRC_SINGLE_SUB)
 
     actual = config.get_substitutions_from_config(config_obj)
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_get_substitution_from_config_finds_multiple_substitutions():
@@ -710,7 +698,7 @@ def test_get_substitution_from_config_finds_multiple_substitutions():
     config_obj = _get_egrc_config(PATH_EGRC_WITH_DATA)
 
     actual = config.get_substitutions_from_config(config_obj)
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def _get_egrc_config(egrc_path):
diff --git a/test/core_test.py b/test/core_test.py
index 9e5d34b..3c9db1e 100644
--- a/test/core_test.py
+++ b/test/core_test.py
@@ -1,6 +1,5 @@
 from collections import namedtuple
 from mock import patch
-from nose.tools import assert_equal
 
 from eg import core
 from test.util_test import _create_config
@@ -109,22 +108,22 @@ def _helper_parses_correctly(
 
     with patch('sys.argv', new=argv):
         actual_args = core._parse_arguments()
-        assert_equal(actual_args.config_file, expected_args.config_file)
-        assert_equal(actual_args.custom_dir, expected_args.custom_dir)
-        assert_equal(actual_args.examples_dir, expected_args.examples_dir)
-        assert_equal(actual_args.list, expected_args.list)
-        assert_equal(actual_args.pager_cmd, expected_args.pager_cmd)
-        assert_equal(actual_args.use_color, expected_args.use_color)
-        assert_equal(actual_args.squeeze, expected_args.squeeze)
-        assert_equal(actual_args.version, expected_args.version)
-        assert_equal(actual_args.edit, expected_args.edit)
+        assert actual_args.config_file == expected_args.config_file
+        assert actual_args.custom_dir == expected_args.custom_dir
+        assert actual_args.examples_dir == expected_args.examples_dir
+        assert actual_args.list == expected_args.list
+        assert actual_args.pager_cmd == expected_args.pager_cmd
+        assert actual_args.use_color == expected_args.use_color
+        assert actual_args.squeeze == expected_args.squeeze
+        assert actual_args.version == expected_args.version
+        assert actual_args.edit == expected_args.edit
         # Note that here we use the default, as described above.
-        assert_equal(actual_args.program, default_program)
+        assert actual_args.program == default_program
 
         # Now make sure we didn't call any other parser methods
-        assert_equal(mock_exit.call_args_list, [])
-        assert_equal(mock_help.call_args_list, [])
-        assert_equal(mock_error.call_args_list, [])
+        assert mock_exit.call_args_list == []
+        assert mock_help.call_args_list == []
+        assert mock_error.call_args_list == []
 
 
 def test_parses_correctly_if_just_program():
@@ -304,29 +303,29 @@ def _helper_run_eg_responds_to_args_correctly(
     if (call_show_list):
         mock_show_list.assert_called_once_with(resolved_config)
     else:
-        assert_equal(mock_show_list.call_args_list, [])
+        assert mock_show_list.call_args_list == []
 
     if (call_show_version):
         mock_show_version.assert_called_once_with()
     else:
-        assert_equal(mock_show_version.call_args_list, [])
+        assert mock_show_version.call_args_list == []
 
     if (call_no_editor):
         mock_no_editor.assert_called_once_with()
     else:
-        assert_equal(mock_no_editor.call_args_list, [])
+        assert mock_no_editor.call_args_list == []
 
     if (call_edit_custom):
         mock_edit_custom.assert_called_once_with(args.program, resolved_config)
     else:
-        assert_equal(mock_edit_custom.call_args_list, [])
+        assert mock_edit_custom.call_args_list == []
 
     if (call_handle_program):
         mock_handle_program.assert_called_once_with(
             args.program, resolved_config
         )
     else:
-        assert_equal(mock_handle_program.call_args_list, [])
+        assert mock_handle_program.call_args_list == []
 
 
 def test_shows_version():
diff --git a/test/substitute_test.py b/test/substitute_test.py
index 7c5ffb2..2b636ab 100644
--- a/test/substitute_test.py
+++ b/test/substitute_test.py
@@ -2,22 +2,20 @@ import re
 
 from eg import substitute
 from mock import patch
-from nose.tools import assert_equal
-from nose.tools import assert_true
 
 
 def test_equality():
     """== should work on Substitution objects."""
     alpha = substitute.Substitution('foo', 'bar', False)
     beta = substitute.Substitution('foo', 'bar', False)
-    assert_true(alpha == beta)
+    assert alpha == beta
 
 
 def test_not_equal():
     """!= should work on Substitution objects."""
     alpha = substitute.Substitution('foo', 'bar', True)
     beta = substitute.Substitution('foo', 'bar', False)
-    assert_true(alpha != beta)
+    assert alpha != beta
 
 
 def test_applies_multiline_substitution():
@@ -29,7 +27,7 @@ def test_applies_multiline_substitution():
     sub = substitute.Substitution(pattern, replacement, True)
 
     actual = sub.apply_and_get_result(raw)
-    assert_equal(actual, subbed)
+    assert actual == subbed
 
 
 def test_applies_normal_mode_substitution():
@@ -41,7 +39,7 @@ def test_applies_normal_mode_substitution():
     sub = substitute.Substitution(pattern, replacement, False)
 
     actual = sub.apply_and_get_result(raw)
-    assert_equal(actual, subbed)
+    assert actual == subbed
 
 
 def test_calls_correct_re_methods_for_multiline():
@@ -60,8 +58,12 @@ def test_calls_correct_re_methods_without_multiline():
     _helper_assert_about_apply_and_get_result(False)
 
 
+@patch('re.sub')
+@patch('re.compile')
 def _helper_assert_about_apply_and_get_result(
-    is_multiline
+    is_multiline,
+    compile_method,
+    sub_method
 ):
     """
     Helper method to assert about the correct results of calls to
@@ -72,22 +74,24 @@ def _helper_assert_about_apply_and_get_result(
     compiled_pattern = 'whoopty compiled'
     subbed_result = 'substituted'
     starting_string = 'the start'
-    with patch('re.compile', return_value=compiled_pattern) as compile_method:
-        with patch('re.sub', return_value=subbed_result) as sub_method:
-            if is_multiline:
-                sub = substitute.Substitution(pattern, repl, True)
-            else:
-                sub = substitute.Substitution(pattern, repl, False)
-            actual = sub.apply_and_get_result(starting_string)
-
-            if is_multiline:
-                compile_method.assert_called_once_with(pattern, re.MULTILINE)
-            else:
-                compile_method.assert_called_once_with(pattern)
-
-            sub_method.assert_called_once_with(
-                compiled_pattern,
-                repl,
-                starting_string
-            )
-            assert_equal(actual, subbed_result)
+
+    compile_method.return_value = compiled_pattern
+    sub_method.return_value = subbed_result
+
+    if is_multiline:
+        sub = substitute.Substitution(pattern, repl, True)
+    else:
+        sub = substitute.Substitution(pattern, repl, False)
+    actual = sub.apply_and_get_result(starting_string)
+
+    if is_multiline:
+        compile_method.assert_called_once_with(pattern, re.MULTILINE)
+    else:
+        compile_method.assert_called_once_with(pattern)
+
+    sub_method.assert_called_once_with(
+        compiled_pattern,
+        repl,
+        starting_string
+    )
+    assert actual == subbed_result
diff --git a/test/util_test.py b/test/util_test.py
index af386d2..d874397 100644
--- a/test/util_test.py
+++ b/test/util_test.py
@@ -6,7 +6,6 @@ from eg import substitute
 from eg import util
 from mock import Mock
 from mock import patch
-from nose.tools import assert_equal
 
 PATH_UNSQUEEZED_FILE = os.path.join(
     'test',
@@ -46,219 +45,122 @@ def _create_config(
     )
 
 
-def test_get_file_path_for_program_correct():
+@patch('os.walk')
+def test_get_file_paths_for_program_with_single(mock_walk):
     program = 'cp'
-    examples_dir = '/Users/tyrion/test/eg_dir'
+    examples_dir = '/Users/tyrion'
     program_file = program + util.EXAMPLE_FILE_SUFFIX
-    target = os.path.join(examples_dir, program_file)
+    expected = ['/Users/tyrion/cp.md']
 
-    actual = util.get_file_path_for_program(program, examples_dir)
-
-    assert_equal(actual, target)
+    mock_walk.return_value = [
+        [examples_dir, [], [program_file, 'cp.txt', 'other_file.md']],
+    ]
 
+    actual = util.get_file_paths_for_program(program, examples_dir)
+    assert actual == expected
+    mock_walk.assert_called_once_with(examples_dir)
 
-def test_has_default_entry_for_program_no_examples_dir():
-    test_config = _create_config(
-        examples_dir=None,
-        custom_dir='customdir',
-    )
 
+@patch('os.walk')
+def test_get_file_paths_for_program_with_nested(mock_walk):
     program = 'cp'
+    examples_dir = '/Users/tyrion'
+    program_file = 'cp.md'
+
+    mock_walk.return_value = [
+        [
+            examples_dir,
+            ['dirA', 'dirB'],
+            [program_file, 'cp.txt', 'other_file.md'],
+        ],
+        [
+            examples_dir + '/dirA',
+            ['dirA-child'],
+            [program_file, 'bad.md'],
+        ],
+        [
+            examples_dir + '/dirA/dirA-child',
+            [],
+            ['bad.md', program_file, 'wtf.md'],
+        ],
+        [
+            examples_dir + '/dirB',
+            [],
+            ['foo.md', program_file],
+        ],
+    ]
 
-    has_entry = util.has_default_entry_for_program(program, test_config)
-
-    assert_equal(False, has_entry)
-
-
-def test_has_custom_entry_for_program_no_custom_dir():
-    test_config = _create_config(
-        examples_dir='examplesdir',
-        custom_dir=None,
-    )
-
-    program = 'find'
+    expected = [
+        '/Users/tyrion/cp.md',
+        '/Users/tyrion/dirA/cp.md',
+        '/Users/tyrion/dirA/dirA-child/cp.md',
+        '/Users/tyrion/dirB/cp.md',
+    ]
 
-    has_entry = util.has_custom_entry_for_program(program, test_config)
+    actual = util.get_file_paths_for_program(program, examples_dir)
+    assert actual == expected
+    mock_walk.assert_called_once_with(examples_dir)
 
-    assert_equal(False, has_entry)
 
+@patch('os.walk')
+def test_get_file_paths_for_program_with_none(mock_walk):
+    expected = []
+    mock_walk.return_value = []
 
-def test_has_default_entry_when_present():
-    test_config = _create_config(
-        examples_dir='examplesdir',
-    )
-    program = 'mv'
+    actual = util.get_file_paths_for_program('cp', '/Users/tyrion')
+    assert actual == expected
+    mock_walk.assert_called_once_with('/Users/tyrion')
 
-    path = '/Users/tyrion/examplesdir/mv.md'
 
-    _helper_assert_path_isfile_not_present(
-        test_config,
-        program,
-        path,
-        'default',
-        True,
-        True
-    )
+@patch('os.walk')
+def test_get_file_paths_for_program_with_no_dir(mock_walk):
+    assert util.get_file_paths_for_program('cp', None) == []
 
 
-def test_has_default_entry_when_not_present():
-    test_config = _create_config(
-        examples_dir='examplesdir',
-    )
+@patch('eg.util.page_string')
+@patch('eg.util.get_formatted_contents')
+@patch('eg.util.get_contents_from_files')
+@patch('eg.util.get_resolved_program')
+def test_handle_program_no_entries(
+    mock_resolve_program,
+    mock_get_contents,
+    mock_format,
+    mock_page_string,
+):
+    """
+    We should do the right thing if there are no entries for a given program.
+    """
     program = 'cp'
+    test_config = _create_config()
 
-    path = '/Users/tyrion/examplesdir/cp.md'
-
-    _helper_assert_path_isfile_not_present(
-        test_config,
-        program,
-        path,
-        'default',
-        False,
-        False,
-    )
-
-
-def test_has_custom_entry_when_present():
-    test_config = _create_config(
-        examples_dir=None,
-        custom_dir='customdir',
-        color_config=None,
-        use_color=False,
-        pager_cmd=None,
-        squeeze=False,
-        subs=None
-    )
-    program = 'find'
+    mock_resolve_program.return_value = program
 
-    path = '/Users/tyrion/customdir/find.md'
+    util.handle_program(program, test_config)
 
-    _helper_assert_path_isfile_not_present(
-        test_config,
+    mock_resolve_program.assert_called_once_with(
         program,
-        path,
-        'custom',
-        True,
-        True
-    )
-
-
-def test_has_custom_entry_when_not_present():
-    test_config = _create_config(
-        custom_dir='customdir',
-        color_config=None,
-        use_color=False,
-        pager_cmd=None,
-        squeeze=False,
-        subs=None
+        test_config
     )
 
-    program = 'locate'
+    # We should have aborted and not called any of the
+    # other methods.
+    assert mock_get_contents.call_count == 0
+    assert mock_format.call_count == 0
+    assert mock_page_string.call_count == 0
 
-    path = '/Users/tyrion/customdir/locate.md'
 
-    _helper_assert_path_isfile_not_present(
-        test_config,
-        program,
-        path,
-        'custom',
-        False,
-        False,
-    )
-
-
-def _helper_assert_path_isfile_not_present(
-        config,
-        program,
-        file_path_for_program,
-        defaultOrCustom,
-        isfile,
-        has_entry
+@patch('eg.util.get_resolved_program')
+@patch('eg.util.get_contents_from_files')
+@patch('eg.util.get_file_paths_for_program')
+@patch('eg.util.get_formatted_contents')
+@patch('eg.util.page_string')
+def test_handle_program_finds_paths_and_calls_open_pager_no_alias(
+    mock_page,
+    mock_format,
+    mock_get_paths,
+    mock_get_contents,
+    mock_resolve,
 ):
-    """
-    Helper for asserting whether or not a default file is present. Pass in the
-    parameters defining the program and directories and say whether or not that
-    file should be found.
-    """
-    if defaultOrCustom != 'default' and defaultOrCustom != 'custom':
-        raise TypeError(
-            'defaultOrCustom must be default or custom, not ' + defaultOrCustom
-        )
-    with patch(
-        'eg.util.get_file_path_for_program',
-        return_value=file_path_for_program
-    ) as mock_get_path:
-        with patch('os.path.isfile', return_value=isfile) as mock_isfile:
-
-            actual = None
-            correct_dir = None
-
-            if (defaultOrCustom == 'default'):
-                correct_dir = config.examples_dir
-                actual = util.has_default_entry_for_program(program, config)
-            else:
-                correct_dir = config.custom_dir
-                actual = util.has_custom_entry_for_program(program, config)
-
-            mock_get_path.assert_called_once_with(program, correct_dir)
-            mock_isfile.assert_called_once_with(file_path_for_program)
-
-            assert_equal(actual, has_entry)
-
-
-def test_handle_program_no_entries():
-    """
-    We should do the right thing if there are no entries for a given program.
-    """
-    program = 'cp'
-    test_config = _create_config()
-
-    with patch(
-        'eg.util.get_resolved_program',
-        return_value=program
-    ) as mock_resolve_program:
-        with patch(
-            'eg.util.has_default_entry_for_program',
-            return_value=False
-        ) as mock_has_default:
-            with patch(
-                'eg.util.has_custom_entry_for_program',
-                return_value=False
-            ) as mock_has_custom:
-                with patch(
-                    'eg.util.get_contents_from_files'
-                ) as mock_get_contents:
-                    with patch(
-                        'eg.util.get_formatted_contents'
-                    ) as mock_format:
-                        with patch(
-                            'eg.util.page_string'
-                        ) as mock_page_string:
-                            util.handle_program(program, test_config)
-
-                            mock_resolve_program.assert_called_once_with(
-                                program,
-                                test_config
-                            )
-
-                            mock_has_default.assert_called_once_with(
-                                program,
-                                test_config
-                            )
-
-                            mock_has_custom.assert_called_once_with(
-                                program,
-                                test_config
-                            )
-
-                            # We should have aborted and not called any of the
-                            # other methods.
-                            assert_equal(mock_get_contents.call_count, 0)
-                            assert_equal(mock_format.call_count, 0)
-                            assert_equal(mock_page_string.call_count, 0)
-
-
-def test_handle_program_finds_paths_and_calls_open_pager_no_alias():
     """
     If there are entries for the program, handle_program needs to get the
     paths, get the contents, format the contents, and page the resulting
@@ -287,8 +189,8 @@ def test_handle_program_finds_paths_and_calls_open_pager_no_alias():
         subs=subs
     )
 
-    default_path = 'test-eg-dir/mv.md'
-    custom_path = 'test-custom-dir/mv.md'
+    default_paths = ['test-eg-dir/mv.md', 'test-eg-dir/foo/mv.md']
+    custom_paths = ['test-custom-dir/mv.md', 'test-custom-dir/bar.md']
 
     def return_correct_path(*args, **kwargs):
         program_param = args[0]
@@ -296,9 +198,9 @@ def test_handle_program_finds_paths_and_calls_open_pager_no_alias():
         if program_param != program:
             raise NameError('expected ' + program + ', got ' + program_param)
         if dir_param == examples_dir:
-            return default_path
+            return default_paths
         elif dir_param == custom_dir:
-            return custom_path
+            return custom_paths
         else:
             raise NameError(
                 'got ' +
@@ -308,76 +210,60 @@ def test_handle_program_finds_paths_and_calls_open_pager_no_alias():
                 ' or ' +
                 custom_dir)
 
-    with patch(
-        'eg.util.get_resolved_program',
-        return_value=program
-    ) as mock_resolve:
-        with patch(
-            'eg.util.has_default_entry_for_program',
-            return_value=True
-        ) as mock_has_default:
-            with patch(
-                'eg.util.has_custom_entry_for_program',
-                return_value=True
-            ) as mock_has_custom:
-                with patch(
-                    'eg.util.get_contents_from_files',
-                    return_value=file_contents
-                ) as mock_get_contents:
-                    with patch(
-                        'eg.util.get_file_path_for_program',
-                        side_effect=return_correct_path
-                    ) as mock_get_file:
-                        with patch(
-                            'eg.util.get_formatted_contents',
-                            return_value=formatted_contents
-                        ) as mock_format:
-                            with patch('eg.util.page_string') as mock_page:
-                                util.handle_program(program, test_config)
-
-                                mock_resolve.assert_called_once_with(
-                                    program,
-                                    test_config
-                                )
-
-                                mock_has_default.assert_called_once_with(
-                                    program,
-                                    test_config
-                                )
-                                mock_has_custom.assert_called_once_with(
-                                    program,
-                                    test_config
-                                )
-
-                                mock_get_file.assert_any_call(
-                                    program,
-                                    examples_dir
-                                )
-                                mock_get_file.assert_any_call(
-                                    program,
-                                    custom_dir,
-                                )
-
-                                mock_get_contents.assert_called_once_with(
-                                    default_path,
-                                    custom_path
-                                )
-
-                                mock_format.assert_called_once_with(
-                                    file_contents,
-                                    use_color=test_config.use_color,
-                                    color_config=test_config.color_config,
-                                    squeeze=test_config.squeeze,
-                                    subs=test_config.subs
-                                )
-
-                                mock_page.assert_called_once_with(
-                                    formatted_contents,
-                                    test_config.pager_cmd
-                                )
-
-
-def test_handle_program_finds_paths_and_calls_open_pager_with_alias():
+    mock_format.return_value = formatted_contents
+    mock_get_paths.side_effect=return_correct_path
+    mock_get_contents.return_value = file_contents
+    mock_resolve.return_value = program
+
+    util.handle_program(program, test_config)
+
+    mock_resolve.assert_called_once_with(
+        program,
+        test_config
+    )
+
+    mock_get_paths.assert_any_call(
+        program,
+        examples_dir
+    )
+    mock_get_paths.assert_any_call(
+        program,
+        custom_dir,
+    )
+
+    mock_get_contents.assert_called_once_with(
+        custom_paths[0],
+        custom_paths[1],
+        default_paths[0],
+        default_paths[1],
+    )
+
+    mock_format.assert_called_once_with(
+        file_contents,
+        use_color=test_config.use_color,
+        color_config=test_config.color_config,
+        squeeze=test_config.squeeze,
+        subs=test_config.subs
+    )
+
+    mock_page.assert_called_once_with(
+        formatted_contents,
+        test_config.pager_cmd
+    )
+
+
+@patch('eg.util.get_resolved_program')
+@patch('eg.util.get_contents_from_files')
+@patch('eg.util.get_file_paths_for_program')
+@patch('eg.util.get_formatted_contents')
+@patch('eg.util.page_string')
+def test_handle_program_finds_paths_and_calls_open_pager_with_alias(
+    mock_page,
+    mock_format,
+    mock_get_paths,
+    mock_get_contents,
+    mock_resolve,
+):
     """
     If there are entries for the program, handle_program needs to get the
     paths, get the contents, format the contents, and page the resulting
@@ -407,8 +293,8 @@ def test_handle_program_finds_paths_and_calls_open_pager_with_alias():
         subs=subs
     )
 
-    default_path = 'test-eg-dir/ln.md'
-    custom_path = 'test-custom-dir/ln.md'
+    default_paths = ['test-eg-dir/ln.md']
+    custom_paths = ['test-custom-dir/ln.md']
 
     def return_correct_path(*args, **kwargs):
         program_param = args[0]
@@ -421,9 +307,9 @@ def test_handle_program_finds_paths_and_calls_open_pager_with_alias():
                 program_param
             )
         if dir_param == examples_dir:
-            return default_path
+            return default_paths
         elif dir_param == custom_dir:
-            return custom_path
+            return custom_paths
         else:
             raise NameError(
                 'got ' +
@@ -433,229 +319,104 @@ def test_handle_program_finds_paths_and_calls_open_pager_with_alias():
                 ' or ' +
                 custom_dir)
 
-    with patch(
-        'eg.util.get_resolved_program',
-        return_value=resolved_program
-    ) as mock_resolve:
-        with patch(
-            'eg.util.has_default_entry_for_program',
-            return_value=True
-        ) as mock_has_default:
-            with patch(
-                'eg.util.has_custom_entry_for_program',
-                return_value=True
-            ) as mock_has_custom:
-                with patch(
-                    'eg.util.get_contents_from_files',
-                    return_value=file_contents
-                ) as mock_get_contents:
-                    with patch(
-                        'eg.util.get_file_path_for_program',
-                        side_effect=return_correct_path
-                    ) as mock_get_file:
-                        with patch(
-                            'eg.util.get_formatted_contents',
-                            return_value=formatted_contents
-                        ) as mock_format:
-                            with patch('eg.util.page_string') as mock_page:
-                                util.handle_program(
-                                    alias_for_program,
-                                    test_config
-                                )
-
-                                mock_resolve.assert_called_once_with(
-                                    alias_for_program,
-                                    test_config
-                                )
-
-                                mock_has_default.assert_called_once_with(
-                                    resolved_program,
-                                    test_config
-                                )
-                                mock_has_custom.assert_called_once_with(
-                                    resolved_program,
-                                    test_config
-                                )
-
-                                mock_get_file.assert_any_call(
-                                    resolved_program,
-                                    examples_dir
-                                )
-                                mock_get_file.assert_any_call(
-                                    resolved_program,
-                                    custom_dir,
-                                )
-
-                                mock_get_contents.assert_called_once_with(
-                                    default_path,
-                                    custom_path
-                                )
-
-                                mock_format.assert_called_once_with(
-                                    file_contents,
-                                    use_color=test_config.use_color,
-                                    color_config=test_config.color_config,
-                                    squeeze=test_config.squeeze,
-                                    subs=test_config.subs
-                                )
-
-                                mock_page.assert_called_once_with(
-                                    formatted_contents,
-                                    test_config.pager_cmd
-                                )
-
-
-def _helper_assert_list_supported_programs(
-    config_obj,
-    default_list,
-    custom_list,
-    alias_dict,
-    target_list
-):
-    """
-    config_obj: Config object to be passed to get_list function
-    default_list: the list of default programs
-    custom_list: the list of programs with custom programs
-    alias_dict: dict of aliases
-    target_list: list of string that should be returned
-    """
-    def give_list(*args, **kwargs):
-        dir_name = args[0]
-        if dir_name == config_obj.custom_dir:
-            return custom_list
-        elif dir_name == config_obj.examples_dir:
-            return default_list
-        else:
-            raise NameError('Not the default or custom dir: ' + dir_name)
-
-    with patch('os.path.isdir', return_value=True):
-        with patch('os.listdir', side_effect=give_list):
-            with patch('eg.util.get_alias_dict', return_value=alias_dict):
-                actual = util.get_list_of_all_supported_commands(config_obj)
-                assert_equal(actual, target_list)
-
+    mock_format.return_value = formatted_contents
+    mock_get_paths.side_effect = return_correct_path
+    mock_get_contents.return_value = file_contents
+    mock_resolve.return_value = resolved_program
 
-def test_list_supported_programs_only_default():
-    example_dir = 'example/dir'
-    custom_dir = 'custom/dir'
-
-    test_config = _create_config(
-        examples_dir=example_dir,
-        custom_dir=custom_dir,
+    util.handle_program(
+        alias_for_program,
+        test_config
     )
 
-    examples_list = ['aliases', 'cp.md', 'find.md', 'xargs.md']
-    custom_list = []
-    target = ['cp', 'find', 'xargs']
-    _helper_assert_list_supported_programs(
-        test_config,
-        examples_list,
-        custom_list,
-        {},
-        target
+    mock_resolve.assert_called_once_with(
+        alias_for_program,
+        test_config
     )
 
-
-def test_list_supported_programs_only_custom():
-    example_dir = 'example/dir'
-    custom_dir = 'custom/dir'
-
-    test_config = _create_config(
-        examples_dir=example_dir,
-        custom_dir=custom_dir,
+    mock_get_paths.assert_any_call(
+        resolved_program,
+        examples_dir
     )
-    target = ['awk +', 'bar +', 'xor +']
-    _helper_assert_list_supported_programs(
-        test_config,
-        [],
-        ['awk.md', 'bar.md', 'xor.md'],
-        {},
-        target
+    mock_get_paths.assert_any_call(
+        resolved_program,
+        custom_dir,
     )
 
+    mock_get_contents.assert_called_once_with(
+        custom_paths[0],
+        default_paths[0]
+    )
 
-def test_list_supported_programs_both():
-    examples_dir = 'example/dir'
-    custom_dir = 'custom/dir'
-
-    test_config = _create_config(
-        examples_dir=examples_dir,
-        custom_dir=custom_dir,
+    mock_format.assert_called_once_with(
+        file_contents,
+        use_color=test_config.use_color,
+        color_config=test_config.color_config,
+        squeeze=test_config.squeeze,
+        subs=test_config.subs
     )
-    examples_list = ['alpha.md', 'bar.md', 'both.md', 'examples.md']
-    custom_list = ['azy.md', 'both.md', 'examples.md', 'zeta.md']
-    target = [
-        'alpha',
-        'azy +',
-        'bar',
-        'both *',
-        'examples *',
-        'zeta +'
-    ]
-    _helper_assert_list_supported_programs(
-        test_config,
-        examples_list,
-        custom_list,
-        {},
-        target
+
+    mock_page.assert_called_once_with(
+        formatted_contents,
+        test_config.pager_cmd
     )
 
 
-def test_list_supported_commands_includes_aliases():
-    examples_dir = 'examples/dir/for/aliases'
-    custom_dir = 'custom/dir/for/aliases'
+def test_get_list_of_all_supported_commands(tmpdir):
+    dir_example = tmpdir.mkdir('examples')
+    dir_custom = tmpdir.mkdir('custom')
 
-    test_config = _create_config(
-        examples_dir=examples_dir,
-        custom_dir=custom_dir,
+    config = _create_config(
+        examples_dir=str(dir_example),
+        custom_dir=str(dir_custom),
     )
-    # Things we want to cover:
-    #   normal alias
-    #   alias that shadows a custom-only declaration
-    #   alias that points to a * or + program
-
-    examples_list = [
-        'alpha.md',
-        'bar.md',
-        'both.md',
-        'default-only.md',
-        'examples.md',
-        'z-hidden-by-alias.md'
-    ]
-    custom_list = [
-        'aaa.md',
-        'azy.md',
-        'both.md',
-        'examples.md',
-        'zeta.md'
+
+    expected = [
+        'a-only-default',
+        'b-both *',
+        'c-only-custom +',
+        'd-only-custom-nested +',
+        'e-only-default-nested',
+        'f-default-custom-nested',
+        'g-both-different-levels *',
+        't-a-only-default-alias -> a-only-default',
+        'u-b-both-alias -> b-both *',
+        'v-c-only-custom-alias -> c-only-custom +'
     ]
-    alias_dict = {
-        'aaa': 'alpha',
-        'y-alias-for-both': 'both',
-        'alias-for-azy': 'azy',
-        'z-hidden-by-alias': 'azy'
+
+    aliases = {
+        't-a-only-default-alias': 'a-only-default',
+        'u-b-both-alias': 'b-both',
+        'v-c-only-custom-alias': 'c-only-custom'
     }
 
-    target = [
-        'aaa -> alpha',  # shadow the custom file
-        'alias-for-azy -> azy +',
-        'alpha',
-        'azy +',
-        'bar',
-        'both *',
-        'default-only',
-        'examples *',
-        'y-alias-for-both -> both *',
-        'z-hidden-by-alias -> azy +',
-        'zeta +'
-    ]
-    _helper_assert_list_supported_programs(
-        test_config,
-        examples_list,
-        custom_list,
-        alias_dict,
-        target
-    )
+    # Make the directory structure we expect.
+    dir_example_nested = dir_example.mkdir('default-nested')
+    dir_custom_nested = dir_custom.mkdir('custom-nested')
+
+    dir_example.join('a-only-default.md').write('foo')
+
+    dir_example.join('b-both.md').write('foo')
+    dir_custom.join('b-both.md').write('foo')
+
+    dir_custom.join('c-only-custom.md').write('foo')
+
+    dir_custom_nested.join('d-only-custom-nested.md').write('foo')
+
+    dir_example_nested.join('e-only-default-nested.md').write('foo')
+
+    dir_example_nested.join('f-default-custom-nested.md').write('foo')
+
+    dir_example.join('g-both-different-levels.md').write('foo')
+    dir_custom_nested.join('g-both-different-levels.md').write('foo')
+
+    # Use the 'with' context manager rather than the @decorator, because the
+    # tmpdir fixture doesn't play nice with the decorator.
+    with patch('eg.util.get_alias_dict') as mock_get_alias:
+        mock_get_alias.return_value = aliases
+        actual = util.get_list_of_all_supported_commands(config)
+        assert actual == expected
+        mock_get_alias.assert_called_once_with(config)
 
 
 def test_list_supported_programs_fails_gracefully_if_no_dirs():
@@ -664,7 +425,7 @@ def test_list_supported_programs_fails_gracefully_if_no_dirs():
     actual = util.get_list_of_all_supported_commands(test_config)
     target = []
 
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_calls_pipepager_if_not_less():
@@ -702,10 +463,14 @@ def test_calls_fallback_if_cmd_is_flag_string():
     )
 
 
+@patch('pydoc.pager')
+@patch('pydoc.pipepager')
 def _helper_assert_about_pager(
     str_to_page,
     pager_cmd,
-    use_fallback
+    use_fallback,
+    pipepager,
+    default_pager,
 ):
     """
     Help with asserting about pager.
@@ -715,62 +480,17 @@ def _helper_assert_about_pager(
     use_default: false if we should actually use pydoc.pipepager, true if we
         instead are going to fallback to pydoc.pager
     """
-    with patch('pydoc.pager') as default_pager:
-        with patch('pydoc.pipepager') as pipepager:
-            util.page_string(str_to_page, pager_cmd)
-
-            if use_fallback:
-                default_pager.assert_called_once_with(str_to_page)
-                assert_equal(pipepager.call_count, 0)
-            else:
-                assert_equal(default_pager.call_count, 0)
-                pipepager.assert_called_once_with(
-                    str_to_page,
-                    cmd=pager_cmd
-                )
-
-
-def _helper_assert_file_contents(
-    default_path,
-    default_contents,
-    custom_path,
-    custom_contents,
-    target_contents
-):
-    """
-    Helper method to assert things about the get_contents_from_files method.
-    Does not actually hit the disk.
-
-    default_path: the path of a default file
-    default_contents: the contents of the default file
-    custom_path: the path to a custom file
-    custom_contents: the contents of the custom file
-    target_contents: the final combined contents that should be returned by the
-        get_contents_from_files method.
-    """
-
-    # This method will be used by the mock framework to return the right file
-    # contents based on the file name.
-    def return_file_contents(*args, **kwargs):
-        if args[0] == default_path:
-            return default_contents
-        elif args[0] == custom_path:
-            return custom_contents
-        else:
-            raise TypeError(
-                args[0] +
-                ' was an unexpected path--should be ' +
-                default_path +
-                ' or ' +
-                custom_path
-            )
+    util.page_string(str_to_page, pager_cmd)
 
-    with patch(
-        'eg.util._get_contents_of_file',
-        side_effect=return_file_contents
-    ):
-        actual = util.get_contents_from_files(default_path, custom_path)
-        assert_equal(actual, target_contents)
+    if use_fallback:
+        default_pager.assert_called_once_with(str_to_page)
+        assert pipepager.call_count == 0
+    else:
+        assert default_pager.call_count == 0
+        pipepager.assert_called_once_with(
+            str_to_page,
+            cmd=pager_cmd
+        )
 
 
 @patch('eg.util.pydoc.pipepager', side_effect=KeyboardInterrupt)
@@ -797,52 +517,87 @@ def test_page_string_excepts_keyboard_interrupt_if_none(pager_mock):
     pager_mock.assert_called_once_with('page me plz')
 
 
-def test_get_contents_from_files_only_default():
+def test_get_contents_from_files_handles_none():
     """
-    Retrieve the correct file contents when only a default file is present.
+    Empty string if no files.
     """
-    default_path = 'test/default/path'
-    default_contents = 'contents of the default file'
     _helper_assert_file_contents(
-        default_path,
-        default_contents,
-        None,
-        None,
-        default_contents
+        [],
+        ''
     )
 
 
-def test_get_contents_from_files_only_custom():
-    """
-    Retrieve only the custom file contents when we only have a custom file
-    path.
-    """
-    custom_path = 'test/custom/path'
-    custom_contents = 'contents of the custom file'
+def test_get_contents_from_files_handles_one():
+    file_infos = [
+        {
+            'path': 'test/path',
+            'contents': 'contents of file'
+        }
+    ]
+    combined_contents = 'contents of file'
     _helper_assert_file_contents(
-        None,
-        None,
-        custom_path,
-        custom_contents,
-        custom_contents
+        file_infos,
+        combined_contents
     )
 
 
-def test_get_contents_from_file_both_default_and_custom():
-    default_path = 'test/default/path'
-    default_contents = 'contents of the default file'
-    custom_path = 'test/custom/path'
-    custom_contents = 'contents of the custom file'
-    combined_contents = custom_contents + default_contents
+def test_get_contents_from_files_handles_multiple():
+    file_infos = [
+        {
+            'path': 'path/1',
+            'contents': 'foo\n'
+        },
+        {
+            'path': 'path/2/foo',
+            'contents': 'bar\n'
+        },
+        {
+            'path': 'another/path',
+            'contents': 'baz'
+        }
+    ]
+
+    combined_contents = 'foo\nbar\nbaz'
+
     _helper_assert_file_contents(
-        default_path,
-        default_contents,
-        custom_path,
-        custom_contents,
+        file_infos,
         combined_contents
     )
 
 
+@patch('eg.util._get_contents_of_file')
+def _helper_assert_file_contents(
+    file_infos,
+    target_contents,
+    get_contents_mock,
+):
+    """
+    Helper method to assert things about the get_contents_from_files method.
+    Does not actually hit the disk.
+
+    file_infos: array of { path, contents } dicts representing files. Array so
+        that we can assert proper order calling
+    target_contents: the final combined contents that should be returned by the
+        get_contents_from_files method.
+    """
+
+    # This method will be used by the mock framework to return the right file
+    # contents based on the file name.
+    def return_file_contents(*args, **kwargs):
+        for file_info in file_infos:
+            if file_info['path'] == args[0]:
+                return file_info['contents']
+        raise TypeError('did not find path in test obj')
+
+    get_contents_mock.side_effect = return_file_contents
+
+    paths = [el['path'] for el in file_infos]
+    actual = util.get_contents_from_files(*paths)
+    assert actual == target_contents
+
+@patch('eg.util.get_colorized_contents')
+@patch('eg.util.get_squeezed_contents')
+@patch('eg.util.get_substituted_contents')
 def _helper_assert_formatted_contents(
     starting_contents,
     use_color,
@@ -852,7 +607,10 @@ def _helper_assert_formatted_contents(
     colorized_contents,
     squeezed_contents,
     subbed_contents,
-    formatted_result
+    formatted_result,
+    sub_method,
+    squeeze_method,
+    color_method,
 ):
     """
     Helper method to assist in asserting things about the
@@ -869,55 +627,47 @@ def _helper_assert_formatted_contents(
     subbed_contents: the result of subbed_contents
     formatted_result: the final, formatted string that should be returned
     """
-    with patch(
-        'eg.util.get_colorized_contents',
-        return_value=colorized_contents
-    ) as color_method:
-        with patch(
-            'eg.util.get_squeezed_contents',
-            return_value=squeezed_contents
-        ) as squeeze_method:
-            with patch(
-                'eg.util.get_substituted_contents',
-                return_value=subbed_contents
-            ) as sub_method:
-                actual = util.get_formatted_contents(
-                    starting_contents,
-                    use_color,
-                    color_config,
-                    squeeze,
-                    subs
-                )
-
-                # We'll update the contents as they get formatted to make sure
-                # we pass the right thing to the various methods.
-                contents_thus_far = starting_contents
-
-                if use_color:
-                    color_method.assert_called_once_with(
-                        contents_thus_far,
-                        color_config
-                    )
-                    contents_thus_far = colorized_contents
-                else:
-                    assert_equal(color_method.call_count, 0)
-
-                if squeeze:
-                    squeeze_method.assert_called_once_with(contents_thus_far)
-                    contents_thus_far = squeezed_contents
-                else:
-                    assert_equal(squeeze_method.call_count, 0)
-
-                if subs:
-                    sub_method.assert_called_once_with(
-                        contents_thus_far,
-                        subs
-                    )
-                    contents_thus_far = subbed_contents
-                else:
-                    assert_equal(sub_method.call_count, 0)
-
-                assert_equal(actual, formatted_result)
+    sub_method.return_value = subbed_contents
+    squeeze_method.return_value = squeezed_contents
+    color_method.return_value = colorized_contents
+
+    actual = util.get_formatted_contents(
+        starting_contents,
+        use_color,
+        color_config,
+        squeeze,
+        subs
+    )
+
+    # We'll update the contents as they get formatted to make sure
+    # we pass the right thing to the various methods.
+    contents_thus_far = starting_contents
+
+    if use_color:
+        color_method.assert_called_once_with(
+            contents_thus_far,
+            color_config
+        )
+        contents_thus_far = colorized_contents
+    else:
+        assert color_method.call_count == 0
+
+    if squeeze:
+        squeeze_method.assert_called_once_with(contents_thus_far)
+        contents_thus_far = squeezed_contents
+    else:
+        assert squeeze_method.call_count == 0
+
+    if subs:
+        sub_method.assert_called_once_with(
+            contents_thus_far,
+            subs
+        )
+        contents_thus_far = subbed_contents
+    else:
+        assert sub_method.call_count == 0
+
+    assert actual == formatted_result
 
 
 def test_get_formatted_contents_does_not_format_methods_if_all_falsey():
@@ -927,15 +677,15 @@ def test_get_formatted_contents_does_not_format_methods_if_all_falsey():
     """
     starting_contents = 'this is where we start'
     _helper_assert_formatted_contents(
-        starting_contents=starting_contents,
-        use_color=False,
-        color_config='some color config',
-        squeeze=False,
-        subs=None,
-        colorized_contents='this was colored',
-        squeezed_contents='this was squeezed',
-        subbed_contents='these contents were subbed',
-        formatted_result=starting_contents
+        starting_contents,
+        False,
+        'some color config',
+        False,
+        None,
+        'this was colored',
+        'this was squeezed',
+        'these contents were subbed',
+        starting_contents
     )
 
 
@@ -946,15 +696,15 @@ def test_get_formatted_contents_calls_colorize_if_use_color():
     starting_contents = 'this is where we start'
     colorized_contents = 'COLORIZED: this is where we start'
     _helper_assert_formatted_contents(
-        starting_contents=starting_contents,
-        use_color=True,
-        color_config='some color config',
-        squeeze=False,
-        subs=None,
-        colorized_contents=colorized_contents,
-        squeezed_contents='this was squeezed',
-        subbed_contents='these contents were subbed',
-        formatted_result=colorized_contents
+        starting_contents,
+        True,
+        'some color config',
+        False,
+        None,
+        colorized_contents,
+        'this was squeezed',
+        'these contents were subbed',
+        colorized_contents
     )
 
 
@@ -963,15 +713,15 @@ def test_get_formatted_contents_squeezes():
     starting_contents = 'this is where we start'
     squeezed_contents = 'this is the result of a squeezing'
     _helper_assert_formatted_contents(
-        starting_contents=starting_contents,
-        use_color=False,
-        color_config='some color config',
-        squeeze=True,
-        subs=None,
-        colorized_contents='this was colored',
-        squeezed_contents=squeezed_contents,
-        subbed_contents='these contents were subbed',
-        formatted_result=squeezed_contents
+        starting_contents,
+        False,
+        'some color config',
+        True,
+        None,
+        'this was colored',
+        squeezed_contents,
+        'these contents were subbed',
+        squeezed_contents
     )
 
 
@@ -980,15 +730,15 @@ def test_get_formatted_contents_subsitutes():
     starting_contents = 'this is where we start'
     subbed_contents = 'substituted like a teacher'
     _helper_assert_formatted_contents(
-        starting_contents=starting_contents,
-        use_color=False,
-        color_config='some color config',
-        squeeze=False,
-        subs=['truthy', 'list'],
-        colorized_contents='this was colored',
-        squeezed_contents='this was squeezed',
-        subbed_contents=subbed_contents,
-        formatted_result=subbed_contents
+        starting_contents,
+        False,
+        'some color config',
+        False,
+        ['truthy', 'list'],
+        'this was colored',
+        'this was squeezed',
+        subbed_contents,
+        subbed_contents
     )
 
 
@@ -1000,15 +750,15 @@ def test_perform_all_formatting():
     starting_contents = 'the starting point for grand formatting'
     subbed_contents = 'subbed is the last thing called so should be the result'
     _helper_assert_formatted_contents(
-        starting_contents=starting_contents,
-        use_color=True,
-        color_config='some color config',
-        squeeze=True,
-        subs=['truthy', 'list'],
-        colorized_contents='this was colored',
-        squeezed_contents='this was squeezed',
-        subbed_contents=subbed_contents,
-        formatted_result=subbed_contents
+        starting_contents,
+        True,
+        'some color config',
+        True,
+        ['truthy', 'list'],
+        'this was colored',
+        'this was squeezed',
+        subbed_contents,
+        subbed_contents
     )
 
 
@@ -1031,14 +781,14 @@ def test_get_squeezed_contents_correctly_squeezes():
     target = _get_file_as_string(PATH_SQUEEZED_FILE)
     actual = util.get_squeezed_contents(unsqueezed)
 
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_get_substituted_contents_handles_empty_subs():
     """Nothing should be formatted if there are no substitutions."""
     raw_contents = 'this should not be subbed'
     actual = util.get_substituted_contents(raw_contents, [])
-    assert_equal(actual, raw_contents)
+    assert actual == raw_contents
 
 
 def test_get_substituted_contents_substitutes_calls_correct_methods():
@@ -1062,7 +812,7 @@ def test_get_substituted_contents_substitutes_calls_correct_methods():
     sub_one.apply_and_get_result.assert_called_once_with(starting_contents)
     sub_two.apply_and_get_result.assert_called_once_with(sub_one_result)
 
-    assert_equal(actual, target)
+    assert actual == target
 
 
 def test_get_substituted_contents_substitutes_correctly():
@@ -1078,10 +828,11 @@ def test_get_substituted_contents_substitutes_correctly():
     subs = [sub_one, sub_two]
     actual = util.get_substituted_contents(start, subs)
 
-    assert_equal(actual, target)
+    assert actual == target
 
 
-def test_get_colorized_contents_calls_methods():
+@patch('eg.color.EgColorizer')
+def test_get_colorized_contents_calls_methods(patched_colorizer_class):
     """
     We should call the correct methods on the EgColorizer objects when we color
     a file.
@@ -1089,22 +840,24 @@ def test_get_colorized_contents_calls_methods():
     raw_contents = 'these are uncolored contents'
     colored_contents = 'COLORED: ' + raw_contents
     color_config = 'some color config'
-    with patch('eg.color.EgColorizer') as patched_colorizer_class:
-        # The actual instance created by these calls is stored at return_value.
-        colorizer_instance = patched_colorizer_class.return_value
-        colorizer_instance.colorize_text.return_value = colored_contents
 
-        actual = util.get_colorized_contents(raw_contents, color_config)
+    # The actual instance created by these calls is stored at return_value.
+    colorizer_instance = patched_colorizer_class.return_value
+    colorizer_instance.colorize_text.return_value = colored_contents
+
+    actual = util.get_colorized_contents(raw_contents, color_config)
 
-        assert_equal(actual, colored_contents)
-        colorizer_instance.colorize_text.assert_called_once_with(raw_contents)
+    assert actual == colored_contents
+    colorizer_instance.colorize_text.assert_called_once_with(raw_contents)
 
 
+@patch('eg.util.get_alias_dict')
 def _helper_assert_get_resolved_program(
     program,
     resolved_program,
     config_obj,
-    alias_dict
+    alias_dict,
+    mock_dict,
 ):
     """
     program: the program to resolved for as an alias
@@ -1112,10 +865,11 @@ def _helper_assert_get_resolved_program(
     config_obj: the config_obj to use toe resolve the alias path
     alias_dict: the dict of aliases to be returned
     """
-    with patch('eg.util.get_alias_dict', return_value=alias_dict) as mock_dict:
-        actual = util.get_resolved_program(program, config_obj)
-        assert_equal(actual, resolved_program)
-        mock_dict.assert_called_once_with(config_obj)
+    mock_dict.return_value = alias_dict
+
+    actual = util.get_resolved_program(program, config_obj)
+    assert actual == resolved_program
+    mock_dict.assert_called_once_with(config_obj)
 
 
 def test_get_resolved_program_no_alias():
@@ -1184,12 +938,18 @@ def test_get_alias_dict_fails_gracefully_if_not_file():
     )
 
 
+@patch('eg.util._get_contents_of_file')
+@patch('eg.util._get_alias_file_path')
+@patch('os.path.isfile')
 def _helper_assert_get_alias_dict(
     contents_of_alias_dict_file,
     target_alias_dict,
     config_obj,
     alias_file_path,
-    alias_file_path_is_file
+    alias_file_path_is_file,
+    mock_is_file,
+    mock_get_alias_file_path,
+    mock_get_contents,
 ):
     """
     contents_of_alias_dict_file: the string contents of the file storing the
@@ -1199,32 +959,25 @@ def _helper_assert_get_alias_dict(
     alias_file_path: the path to be returned by _get_alias_file_path
     alias_file_path_is_file: True if the alias path is a file, else False
     """
-    with patch(
-        'eg.util._get_contents_of_file',
-        return_value=contents_of_alias_dict_file
-    ) as mock_get_contents:
-        with patch(
-            'eg.util._get_alias_file_path',
-            return_value=alias_file_path
-        ) as mock_get_alias_file_path:
-            with patch(
-                'os.path.isfile',
-                return_value=alias_file_path_is_file
-            ) as mock_is_file:
-                actual = util.get_alias_dict(config_obj)
+    mock_is_file.return_value = alias_file_path_is_file
+    mock_get_alias_file_path.return_value = alias_file_path
+    mock_get_contents.return_value = contents_of_alias_dict_file
 
-                assert_equal(actual, target_alias_dict)
+    actual = util.get_alias_dict(config_obj)
 
-                mock_get_alias_file_path.assert_called_once_with(config_obj)
-                mock_is_file.assert_called_once_with(alias_file_path)
+    assert actual == target_alias_dict
 
-                if alias_file_path_is_file:
-                    mock_get_contents.assert_called_once_with(alias_file_path)
-                else:
-                    assert_equal(mock_get_contents.call_count, 0)
+    mock_get_alias_file_path.assert_called_once_with(config_obj)
+    mock_is_file.assert_called_once_with(alias_file_path)
 
+    if alias_file_path_is_file:
+        mock_get_contents.assert_called_once_with(alias_file_path)
+    else:
+        assert mock_get_contents.call_count == 0
 
-def test_get_alias_file_path():
+
+@patch('os.path.join')
+def test_get_alias_file_path(mock_join):
     """
     _get_alias_file_path should just join the example dir and the alias file
     name, to make sure we look in the right place for the file.
@@ -1234,13 +987,14 @@ def test_get_alias_file_path():
     )
 
     join_result = 'joined path'
-    with patch('os.path.join', return_value=join_result) as mock_join:
-        actual = util._get_alias_file_path(config_obj)
-        assert_equal(actual, join_result)
-        mock_join.assert_called_once_with(
-            config_obj.examples_dir,
-            util.ALIAS_FILE_NAME
-        )
+    mock_join.return_value = join_result
+
+    actual = util._get_alias_file_path(config_obj)
+    assert actual == join_result
+    mock_join.assert_called_once_with(
+        config_obj.examples_dir,
+        util.ALIAS_FILE_NAME
+    )
 
 
 def test_is_example_file_true_if_has_suffix():
@@ -1249,7 +1003,7 @@ def test_is_example_file_true_if_has_suffix():
     """
     file_name = 'find.md'
     actual = util._is_example_file(file_name)
-    assert_equal(actual, True)
+    assert actual == True
 
 
 def test_is_example_file_true_if_not_suffix():
@@ -1258,7 +1012,7 @@ def test_is_example_file_true_if_not_suffix():
     """
     file_name = 'aliases.json'
     actual = util._is_example_file(file_name)
-    assert_equal(actual, False)
+    assert actual == False
 
 
 def test_can_parse_alias_file():
@@ -1275,17 +1029,17 @@ def test_can_parse_alias_file():
     alias_file_contents = util._get_contents_of_file(alias_file_path)
     alias_dict = json.loads(alias_file_contents)
     # We'll check that link goes to ln, as we know that one will be present.
-    assert_equal(alias_dict['link'], 'ln')
+    assert alias_dict['link'] == 'ln'
 
 
 @patch('os.path.exists')
 @patch('eg.util._inform_cannot_edit_no_custom_dir')
 @patch('eg.util.get_resolved_program')
-@patch('eg.util.get_file_path_for_program')
+@patch('eg.util.get_file_paths_for_program')
 @patch('subprocess.call')
 def test_edit_custom_examples_correct_with_custom_dir(
     mock_call,
-    mock_get_path,
+    mock_get_paths,
     mock_get_program,
     mock_inform,
     mock_exists,
@@ -1296,28 +1050,59 @@ def test_edit_custom_examples_correct_with_custom_dir(
     program = 'du'
     resolved_program = 'alias for du'
     config = _create_config(custom_dir='path/to/custom', editor_cmd='nano')
-    path = 'path/to/custom/du.md'
+    paths = ['path/to/custom/du.md', 'foo.md']
+
+    mock_get_program.return_value = resolved_program
+    mock_get_paths.return_value = paths
+    mock_exists.return_value = True
+
+    util.edit_custom_examples(program, config)
+
+    mock_get_program.assert_called_once_with(program, config)
+    mock_get_paths.assert_called_once_with(resolved_program, config.custom_dir)
+    mock_call.assert_called_once_with([config.editor_cmd, paths[0]])
+    assert mock_inform.call_count == 0
+
+
+@patch('os.path.exists')
+@patch('eg.util._inform_cannot_edit_no_custom_dir')
+@patch('eg.util.get_resolved_program')
+@patch('eg.util.get_file_paths_for_program')
+@patch('subprocess.call')
+def test_edit_custom_examples_creates_file_if_none_exist(
+    mock_call,
+    mock_get_paths,
+    mock_get_program,
+    mock_inform,
+    mock_exists,
+):
+    program = 'du'
+    resolved_program = 'alias-for-du'
+    config = _create_config(custom_dir='path/to/custom', editor_cmd='nano')
+    paths = []
 
     mock_get_program.return_value = resolved_program
-    mock_get_path.return_value = path
+    mock_get_paths.return_value = paths
     mock_exists.return_value = True
 
     util.edit_custom_examples(program, config)
 
     mock_get_program.assert_called_once_with(program, config)
-    mock_get_path.assert_called_once_with(resolved_program, config.custom_dir)
-    mock_call.assert_called_once_with([config.editor_cmd, path])
-    assert_equal(mock_inform.call_count, 0)
+    mock_get_paths.assert_called_once_with(resolved_program, config.custom_dir)
+    mock_call.assert_called_once_with(
+        [config.editor_cmd, 'path/to/custom/alias-for-du.md'])
+    assert mock_inform.call_count == 0
+
 
 
 @patch('os.path.exists')
 @patch('eg.util._inform_cannot_edit_no_custom_dir')
 @patch('eg.util.get_resolved_program')
-@patch('eg.util.get_file_path_for_program')
+@patch('eg.util.get_file_paths_for_program')
 @patch('subprocess.call')
 def test_edit_custom_examples_informs_if_no_custom_dir(
     mock_call,
-    mock_get_path,
+    mock_get_paths,
     mock_get_program,
     mock_inform,
     mock_exists,
@@ -1333,14 +1118,14 @@ def test_edit_custom_examples_informs_if_no_custom_dir(
     config = _create_config(editor_cmd='vi -e')
     mock_exists.return_value = True
     util.edit_custom_examples(program, config)
-    assert_equal(mock_inform.call_count, 1)
+    assert mock_inform.call_count == 1
 
     # And now with it set but a nonexistent path.
     config = _create_config(custom_dir='/path/to/custom', editor_cmd='vi -e')
     mock_exists.return_value = False
     util.edit_custom_examples(program, config)
-    assert_equal(mock_inform.call_count, 2)
+    assert mock_inform.call_count == 2
 
-    assert_equal(mock_call.call_count, 0)
-    assert_equal(mock_get_path.call_count, 0)
-    assert_equal(mock_get_program.call_count, 0)
+    assert mock_call.call_count == 0
+    assert mock_get_paths.call_count == 0
+    assert mock_get_program.call_count == 0