# Buggy code
def recurse(
    name,
    source,
    clean=False,
    require=None,
    user=None,
    group=None,
    dir_mode=None,
    file_mode=None,
    sym_mode=None,
    template=None,
    context=None,
    defaults=None,
    env=None,
    include_empty=False,
    backup="",
    include_pat=None,
    exclude_pat=None,
    maxdepth=None,
    keep_symlinks=False,
    force_symlinks=False,
    **kwargs,
):
    """
    Recurse through a subdirectory on the master and copy said subdirectory
    over to the specified path.

    name
        The directory to set the recursion in

    source
        The source directory, this directory is located on the salt master file
        server and is specified with the salt:// protocol. If the directory is
        located on the master in the directory named spam, and is called eggs,
        the source string is salt://spam/eggs

    clean
        Make sure that only files that are set up by salt and required by this
        function are kept. If this option is set then everything in this
        directory will be deleted unless it is required.

    require
        Require other resources such as packages or files

    user
        The user to own the directory, this defaults to the user salt is
        running as on the minion

    group
        The group ownership set for the directory, this defaults to the group
        salt is running as on the minion

    dir_mode
        The permissions mode to set any directories created

    file_mode
        The permissions mode to set any files created

    sym_mode
        The permissions mode to set on any symlink created

    template
        If this setting is applied then the named templating engine will be
        used to render the downloaded file, currently jinja, mako, and wempy
        are supported

    .. note::

        The template option is required when recursively applying templates.

    context
        Overrides default context variables passed to the template.

    defaults
        Default context passed to the template.

    include_empty
        Set this to True if empty directories should also be created
        (default is False)

    include_pat
        When copying, include only this pattern from the source. Default
        is glob match; if prefixed with 'E@', then regexp match.
        Example::

          - include_pat: hello*       :: glob matches 'hello01', 'hello02'
                                         ... but not 'otherhello'
          - include_pat: E@hello      :: regexp matches 'otherhello',
                                         'hello01' ...

    exclude_pat
        When copying, exclude this pattern from the source. If both
        include_pat and exclude_pat are supplied, then it will apply
        conditions cumulatively. i.e. first select based on include_pat, and
        then within that result apply exclude_pat.

        Also, when 'clean=True', exclude this pattern from the removal
        list and preserve in the destination.
        Example::

          - exclude_pat: APPDATA*               :: glob matches APPDATA.01,
                                                   APPDATA.02,.. for exclusion
          - exclude_pat: E@(APPDATA)|(TEMPDATA) :: regexp matches APPDATA
                                                   or TEMPDATA for exclusion

    maxdepth
        When copying, only copy paths which are depth maxdepth from the source
        path.
        Example::

          - maxdepth: 0      :: Only include files located in the source
                                directory
          - maxdepth: 1      :: Only include files located in the source
                                or immediate subdirectories

    keep_symlinks
        Keep symlinks when copying from the source. This option will cause
        the copy operation to terminate at the symlink. If you are after
        rsync-ish behavior, then set this to True.

    force_symlinks
        Force symlink creation. This option will force the symlink creation.
        If a file or directory is obstructing symlink creation it will be
        recursively removed so that symlink creation can proceed. This
        option is usually not needed except in special circumstances.
    """
    user = _test_owner(kwargs, user=user)
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": {},  # { path: [comment, ...] }
    }

    if "mode" in kwargs:
        ret["result"] = False
        ret["comment"] = (
            "'mode' is not allowed in 'file.recurse'. Please use "
            "'file_mode' and 'dir_mode'."
        )
        return ret

    # Make sure that leading zeros stripped by YAML loader are added back
    dir_mode = __salt__["config.manage_mode"](dir_mode)
    file_mode = __salt__["config.manage_mode"](file_mode)

    u_check = _check_user(user, group)
    if u_check:
        # The specified user or group do not exist
        return _error(ret, u_check)
    if not os.path.isabs(name):
        return _error(ret, "Specified file {0} is not an absolute path".format(name))

    if isinstance(env, salt._compat.string_types):
        msg = (
            "Passing a salt environment should be done using 'saltenv' not "
            "'env'. This warning will go away in Salt Boron and this "
            "will be the default and expected behaviour. Please update your "
            "state files."
        )
        salt.utils.warn_until("Boron", msg)
        ret.setdefault("warnings", []).append(msg)
        # No need to set __env__ = env since that's done in the state machinery

    # Verify the source exists.
    try:
        _src_proto, _src_path = source.split("://", 1)
    except ValueError:
        ret["result"] = False
        ret["comment"] = (
            "Could not parse source!\nDid you remember to specify a protocol like salt:// ?"
        )
        return ret

    if not _src_path:
        pass
    elif _src_path.strip("/") not in __salt__["cp.list_master_dirs"](__env__):
        ret["result"] = False
        ret["comment"] = "The source: {0} does not exist on the master".format(source)
        return ret

    # Verify the target directory
    if not os.path.isdir(name):
        if os.path.exists(name):
            # it is not a dir, but it exists - fail out
            return _error(
                ret, "The path {0} exists and is not a directory".format(name)
            )
        if not __opts__["test"]:
            __salt__["file.makedirs_perms"](
                name, user, group, int(str(dir_mode), 8) if dir_mode else None
            )

    def add_comment(path, comment):
        comments = ret["comment"].setdefault(path, [])
        if isinstance(comment, basestring):
            comments.append(comment)
        else:
            comments.extend(comment)

    def merge_ret(path, _ret):
        # Use the most "negative" result code (out of True, None, False)
        if _ret["result"] is False or ret["result"] is True:
            ret["result"] = _ret["result"]

        # Only include comments about files that changed
        if _ret["result"] is not True and _ret["comment"]:
            add_comment(path, _ret["comment"])

        if _ret["changes"]:
            ret["changes"][path] = _ret["changes"]

    def manage_file(path, source):
        source = "{0}|{1}".format(source[:7], source[7:])
        if clean and os.path.exists(path) and os.path.isdir(path):
            _ret = {"name": name, "changes": {}, "result": True, "comment": ""}
            if __opts__["test"]:
                _ret["comment"] = "Replacing directory {0} with a file".format(path)
                _ret["result"] = None
                merge_ret(path, _ret)
                return
            else:
                shutil.rmtree(path)
                _ret["changes"] = {"diff": "Replaced directory with a new file"}
                merge_ret(path, _ret)

        # Conflicts can occur if some kwargs are passed in here
        pass_kwargs = {}
        faults = ["mode", "makedirs", "replace"]
        for key in kwargs:
            if key not in faults:
                pass_kwargs[key] = kwargs[key]

        _ret = managed(
            path,
            source=source,
            user=user,
            group=group,
            mode=file_mode,
            template=template,
            makedirs=True,
            context=context,
            replace=True,
            defaults=defaults,
            backup=backup,
            **pass_kwargs,
        )
        merge_ret(path, _ret)

    def manage_directory(path):
        if os.path.basename(path) == "..":
            return
        if clean and os.path.exists(path) and not os.path.isdir(path):
            _ret = {"name": name, "changes": {}, "result": True, "comment": ""}
            if __opts__["test"]:
                _ret["comment"] = "Replacing {0} with a directory".format(path)
                _ret["result"] = None
                merge_ret(path, _ret)
                return
            else:
                os.remove(path)
                _ret["changes"] = {"diff": "Replaced file with a directory"}
                merge_ret(path, _ret)

        _ret = directory(
            path,
            user=user,
            group=group,
            recurse=[],
            dir_mode=dir_mode,
            file_mode=None,
            makedirs=True,
            clean=False,
            require=None,
        )
        merge_ret(path, _ret)

    # Process symlinks and return the updated filenames list
    def process_symlinks(filenames, symlinks):
        for lname, ltarget in symlinks.items():
            if not _check_include_exclude(
                os.path.relpath(lname, srcpath), include_pat, exclude_pat
            ):
                continue
            srelpath = os.path.relpath(lname, srcpath)
            # Check for max depth
            if maxdepth is not None:
                srelpieces = srelpath.split("/")
                if not srelpieces[-1]:
                    srelpieces = srelpieces[:-1]
                if len(srelpieces) > maxdepth + 1:
                    continue
            # Check for all paths that begin with the symlink
            # and axe it leaving only the dirs/files below it.
            _filenames = filenames
            for filename in _filenames:
                if filename.startswith(lname):
                    log.debug(
                        "** skipping file ** {0}, it intersects a symlink".format(
                            filename
                        )
                    )
                    filenames.remove(filename)
            # Create the symlink along with the necessary dirs.
            # The dir perms/ownership will be adjusted later
            # if needed
            _ret = symlink(
                os.path.join(name, srelpath),
                ltarget,
                makedirs=True,
                force=force_symlinks,
                user=user,
                group=group,
                mode=sym_mode,
            )
            if not _ret:
                continue
            merge_ret(os.path.join(name, srelpath), _ret)
            # Add the path to the keep set in case clean is set to True
            keep.add(os.path.join(name, srelpath))
        return filenames

    # If source is a list, find which in the list actually exists
    source, source_hash = __salt__["file.source_list"](source, "", __env__)

    keep = set()
    vdir = set()
    srcpath = source[7:]
    if not srcpath.endswith("/"):
        # we're searching for things that start with this *directory*.
        # use '/' since #master only runs on POSIX
        srcpath = srcpath + "/"
    fns_ = __salt__["cp.list_master"](__env__, srcpath)
    # If we are instructed to keep symlinks, then process them.
    if keep_symlinks:
        # Make this global so that emptydirs can use it if needed.
        symlinks = __salt__["cp.list_master_symlinks"](__env__, srcpath)
        fns_ = process_symlinks(fns_, symlinks)
    for fn_ in fns_:
        if not fn_.strip():
            continue

        # fn_ here is the absolute (from file_roots) source path of
        # the file to copy from; it is either a normal file or an
        # empty dir(if include_empty==true).

        relname = os.path.relpath(fn_, srcpath)
        if relname.startswith(".."):
            continue

        # Check for maxdepth of the relative path
        if maxdepth is not None:
            # Since paths are all master, just use POSIX separator
            relpieces = relname.split("/")
            # Handle empty directories (include_empty==true) by removing the
            # the last piece if it is an empty string
            if not relpieces[-1]:
                relpieces = relpieces[:-1]
            if len(relpieces) > maxdepth + 1:
                continue

        # - Check if it is to be excluded. Match only part of the path
        # relative to the target directory
        if not _check_include_exclude(relname, include_pat, exclude_pat):
            continue
        dest = os.path.join(name, relname)
        dirname = os.path.dirname(dest)
        keep.add(dest)

        if dirname not in vdir:
            # verify the directory perms if they are set
            manage_directory(dirname)
            vdir.add(dirname)

        src = "salt://{0}".format(fn_)
        manage_file(dest, src)

    if include_empty:
        mdirs = __salt__["cp.list_master_dirs"](__env__, srcpath)
        for mdir in mdirs:
            if not _check_include_exclude(
                os.path.relpath(mdir, srcpath), include_pat, exclude_pat
            ):
                continue
            mdest = os.path.join(name, os.path.relpath(mdir, srcpath))
            # Check for symlinks that happen to point to an empty dir.
            if keep_symlinks:
                for link in symlinks:
                    if mdir.startswith(link, 0):
                        log.debug(
                            "** skipping empty dir ** {0}, it intersects"
                            " a symlink".format(mdir)
                        )
                    continue
            manage_directory(mdest)
            keep.add(mdest)

    keep = list(keep)
    if clean:
        # TODO: Use directory(clean=True) instead
        keep += _gen_keep_files(name, require)
        removed = _clean_dir(name, list(keep), exclude_pat)
        if removed:
            if __opts__["test"]:
                if ret["result"]:
                    ret["result"] = None
                add_comment("removed", removed)
            else:
                ret["changes"]["removed"] = removed

    # Flatten comments until salt command line client learns
    # to display structured comments in a readable fashion
    ret["comment"] = "\n".join(
        "\n#### {0} ####\n{1}".format(
            k, v if isinstance(v, basestring) else "\n".join(v)
        )
        for (k, v) in ret["comment"].iteritems()
    ).strip()

    if not ret["comment"]:
        ret["comment"] = "Recursively updated {0}".format(name)

    if not ret["changes"] and ret["result"]:
        ret["comment"] = "The directory {0} is in the correct state".format(name)

    return ret


# Traceback (TypeError)
State: - file
Name:      /var/spool/cron/root
Function:  managed
Result:    False
Comment:   An exception occurred in this state: Traceback (most recent call last):
File "/usr/lib/python2.6/site-packages/salt/state.py", line 1258, in call
*cdata['args'], **cdata['kwargs'])
File "/usr/lib/python2.6/site-packages/salt/states/file.py", line 1091, in managed
**kwargs
File "/usr/lib/python2.6/site-packages/salt/modules/file.py", line 1770, in check_managed
**kwargs)
File "/usr/lib/python2.6/site-packages/salt/modules/file.py", line 1588, in get_managed
if salt._compat.urlparse(source).scheme == 'salt':
File "/usr/lib64/python2.6/urlparse.py", line 129, in urlparse
tuple = urlsplit(url, scheme, allow_fragments)
File "/usr/lib64/python2.6/urlparse.py", line 162, in urlsplit
cached = _parse_cache.get(key, None)
TypeError: unhashable type: 'list'

# Fixed code
def recurse(
    name,
    source,
    clean=False,
    require=None,
    user=None,
    group=None,
    dir_mode=None,
    file_mode=None,
    sym_mode=None,
    template=None,
    context=None,
    defaults=None,
    env=None,
    include_empty=False,
    backup="",
    include_pat=None,
    exclude_pat=None,
    maxdepth=None,
    keep_symlinks=False,
    force_symlinks=False,
    **kwargs,
):
    """
    Recurse through a subdirectory on the master and copy said subdirectory
    over to the specified path.

    name
        The directory to set the recursion in

    source
        The source directory, this directory is located on the salt master file
        server and is specified with the salt:// protocol. If the directory is
        located on the master in the directory named spam, and is called eggs,
        the source string is salt://spam/eggs

    clean
        Make sure that only files that are set up by salt and required by this
        function are kept. If this option is set then everything in this
        directory will be deleted unless it is required.

    require
        Require other resources such as packages or files

    user
        The user to own the directory, this defaults to the user salt is
        running as on the minion

    group
        The group ownership set for the directory, this defaults to the group
        salt is running as on the minion

    dir_mode
        The permissions mode to set any directories created

    file_mode
        The permissions mode to set any files created

    sym_mode
        The permissions mode to set on any symlink created

    template
        If this setting is applied then the named templating engine will be
        used to render the downloaded file, currently jinja, mako, and wempy
        are supported

    .. note::

        The template option is required when recursively applying templates.

    context
        Overrides default context variables passed to the template.

    defaults
        Default context passed to the template.

    include_empty
        Set this to True if empty directories should also be created
        (default is False)

    include_pat
        When copying, include only this pattern from the source. Default
        is glob match; if prefixed with 'E@', then regexp match.
        Example::

          - include_pat: hello*       :: glob matches 'hello01', 'hello02'
                                         ... but not 'otherhello'
          - include_pat: E@hello      :: regexp matches 'otherhello',
                                         'hello01' ...

    exclude_pat
        When copying, exclude this pattern from the source. If both
        include_pat and exclude_pat are supplied, then it will apply
        conditions cumulatively. i.e. first select based on include_pat, and
        then within that result apply exclude_pat.

        Also, when 'clean=True', exclude this pattern from the removal
        list and preserve in the destination.
        Example::

          - exclude_pat: APPDATA*               :: glob matches APPDATA.01,
                                                   APPDATA.02,.. for exclusion
          - exclude_pat: E@(APPDATA)|(TEMPDATA) :: regexp matches APPDATA
                                                   or TEMPDATA for exclusion

    maxdepth
        When copying, only copy paths which are depth maxdepth from the source
        path.
        Example::

          - maxdepth: 0      :: Only include files located in the source
                                directory
          - maxdepth: 1      :: Only include files located in the source
                                or immediate subdirectories

    keep_symlinks
        Keep symlinks when copying from the source. This option will cause
        the copy operation to terminate at the symlink. If you are after
        rsync-ish behavior, then set this to True.

    force_symlinks
        Force symlink creation. This option will force the symlink creation.
        If a file or directory is obstructing symlink creation it will be
        recursively removed so that symlink creation can proceed. This
        option is usually not needed except in special circumstances.
    """
    user = _test_owner(kwargs, user=user)
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": {},  # { path: [comment, ...] }
    }

    if "mode" in kwargs:
        ret["result"] = False
        ret["comment"] = (
            "'mode' is not allowed in 'file.recurse'. Please use "
            "'file_mode' and 'dir_mode'."
        )
        return ret

    # Make sure that leading zeros stripped by YAML loader are added back
    dir_mode = __salt__["config.manage_mode"](dir_mode)
    file_mode = __salt__["config.manage_mode"](file_mode)

    u_check = _check_user(user, group)
    if u_check:
        # The specified user or group do not exist
        return _error(ret, u_check)
    if not os.path.isabs(name):
        return _error(ret, "Specified file {0} is not an absolute path".format(name))

    if isinstance(env, salt._compat.string_types):
        msg = (
            "Passing a salt environment should be done using 'saltenv' not "
            "'env'. This warning will go away in Salt Boron and this "
            "will be the default and expected behaviour. Please update your "
            "state files."
        )
        salt.utils.warn_until("Boron", msg)
        ret.setdefault("warnings", []).append(msg)
        # No need to set __env__ = env since that's done in the state machinery

    # Handle corner case where someone uses a numeric source
    if isinstance(source, (integer_types, float)):
        ret["result"] = False
        ret["comment"] = "Invalid source {0} (cannot be numeric)".format(source)
        return ret

    # Make sure that only salt fileserver paths are being used (no http(s)/ftp)
    if isinstance(source, string_types):
        source_precheck = [source]
    else:
        source_precheck = source
    for precheck in source_precheck:
        if not precheck.startswith("salt://"):
            ret["result"] = False
            ret["comment"] = "Invalid source {0!r} (must be a salt:// URI)".format(
                precheck
            )
            return ret

    # If source is a list, find which in the list actually exists
    try:
        source, source_hash = __salt__["file.source_list"](source, "", __env__)
    except CommandExecutionError as exc:
        ret["result"] = False
        ret["comment"] = "Recurse failed: {0}".format(exc)
        return ret

    # Check source path relative to fileserver root, make sure it is a
    # directory
    source_rel = source.partition("://")[2]
    if source_rel not in __salt__["cp.list_master_dirs"](__env__):
        ret["result"] = False
        ret["comment"] = (
            "The directory {0!r} does not exist on the salt fileserver".format(source)
        )
        return ret

    # Verify the target directory
    if not os.path.isdir(name):
        if os.path.exists(name):
            # it is not a dir, but it exists - fail out
            return _error(
                ret, "The path {0} exists and is not a directory".format(name)
            )
        if not __opts__["test"]:
            __salt__["file.makedirs_perms"](
                name, user, group, int(str(dir_mode), 8) if dir_mode else None
            )

    def add_comment(path, comment):
        comments = ret["comment"].setdefault(path, [])
        if isinstance(comment, basestring):
            comments.append(comment)
        else:
            comments.extend(comment)

    def merge_ret(path, _ret):
        # Use the most "negative" result code (out of True, None, False)
        if _ret["result"] is False or ret["result"] is True:
            ret["result"] = _ret["result"]

        # Only include comments about files that changed
        if _ret["result"] is not True and _ret["comment"]:
            add_comment(path, _ret["comment"])

        if _ret["changes"]:
            ret["changes"][path] = _ret["changes"]

    def manage_file(path, source):
        source = "{0}|{1}".format(source[:7], source[7:])
        if clean and os.path.exists(path) and os.path.isdir(path):
            _ret = {"name": name, "changes": {}, "result": True, "comment": ""}
            if __opts__["test"]:
                _ret["comment"] = "Replacing directory {0} with a file".format(path)
                _ret["result"] = None
                merge_ret(path, _ret)
                return
            else:
                shutil.rmtree(path)
                _ret["changes"] = {"diff": "Replaced directory with a new file"}
                merge_ret(path, _ret)

        # Conflicts can occur if some kwargs are passed in here
        pass_kwargs = {}
        faults = ["mode", "makedirs", "replace"]
        for key in kwargs:
            if key not in faults:
                pass_kwargs[key] = kwargs[key]

        _ret = managed(
            path,
            source=source,
            user=user,
            group=group,
            mode=file_mode,
            template=template,
            makedirs=True,
            context=context,
            replace=True,
            defaults=defaults,
            backup=backup,
            **pass_kwargs,
        )
        merge_ret(path, _ret)

    def manage_directory(path):
        if os.path.basename(path) == "..":
            return
        if clean and os.path.exists(path) and not os.path.isdir(path):
            _ret = {"name": name, "changes": {}, "result": True, "comment": ""}
            if __opts__["test"]:
                _ret["comment"] = "Replacing {0} with a directory".format(path)
                _ret["result"] = None
                merge_ret(path, _ret)
                return
            else:
                os.remove(path)
                _ret["changes"] = {"diff": "Replaced file with a directory"}
                merge_ret(path, _ret)

        _ret = directory(
            path,
            user=user,
            group=group,
            recurse=[],
            dir_mode=dir_mode,
            file_mode=None,
            makedirs=True,
            clean=False,
            require=None,
        )
        merge_ret(path, _ret)

    # Process symlinks and return the updated filenames list
    def process_symlinks(filenames, symlinks):
        for lname, ltarget in symlinks.items():
            if not _check_include_exclude(
                os.path.relpath(lname, srcpath), include_pat, exclude_pat
            ):
                continue
            srelpath = os.path.relpath(lname, srcpath)
            # Check for max depth
            if maxdepth is not None:
                srelpieces = srelpath.split("/")
                if not srelpieces[-1]:
                    srelpieces = srelpieces[:-1]
                if len(srelpieces) > maxdepth + 1:
                    continue
            # Check for all paths that begin with the symlink
            # and axe it leaving only the dirs/files below it.
            _filenames = filenames
            for filename in _filenames:
                if filename.startswith(lname):
                    log.debug(
                        "** skipping file ** {0}, it intersects a symlink".format(
                            filename
                        )
                    )
                    filenames.remove(filename)
            # Create the symlink along with the necessary dirs.
            # The dir perms/ownership will be adjusted later
            # if needed
            _ret = symlink(
                os.path.join(name, srelpath),
                ltarget,
                makedirs=True,
                force=force_symlinks,
                user=user,
                group=group,
                mode=sym_mode,
            )
            if not _ret:
                continue
            merge_ret(os.path.join(name, srelpath), _ret)
            # Add the path to the keep set in case clean is set to True
            keep.add(os.path.join(name, srelpath))
        return filenames

    keep = set()
    vdir = set()
    srcpath = source[7:]
    if not srcpath.endswith("/"):
        # we're searching for things that start with this *directory*.
        # use '/' since #master only runs on POSIX
        srcpath = srcpath + "/"
    fns_ = __salt__["cp.list_master"](__env__, srcpath)
    # If we are instructed to keep symlinks, then process them.
    if keep_symlinks:
        # Make this global so that emptydirs can use it if needed.
        symlinks = __salt__["cp.list_master_symlinks"](__env__, srcpath)
        fns_ = process_symlinks(fns_, symlinks)
    for fn_ in fns_:
        if not fn_.strip():
            continue

        # fn_ here is the absolute (from file_roots) source path of
        # the file to copy from; it is either a normal file or an
        # empty dir(if include_empty==true).

        relname = os.path.relpath(fn_, srcpath)
        if relname.startswith(".."):
            continue

        # Check for maxdepth of the relative path
        if maxdepth is not None:
            # Since paths are all master, just use POSIX separator
            relpieces = relname.split("/")
            # Handle empty directories (include_empty==true) by removing the
            # the last piece if it is an empty string
            if not relpieces[-1]:
                relpieces = relpieces[:-1]
            if len(relpieces) > maxdepth + 1:
                continue

        # - Check if it is to be excluded. Match only part of the path
        # relative to the target directory
        if not _check_include_exclude(relname, include_pat, exclude_pat):
            continue
        dest = os.path.join(name, relname)
        dirname = os.path.dirname(dest)
        keep.add(dest)

        if dirname not in vdir:
            # verify the directory perms if they are set
            manage_directory(dirname)
            vdir.add(dirname)

        src = "salt://{0}".format(fn_)
        manage_file(dest, src)

    if include_empty:
        mdirs = __salt__["cp.list_master_dirs"](__env__, srcpath)
        for mdir in mdirs:
            if not _check_include_exclude(
                os.path.relpath(mdir, srcpath), include_pat, exclude_pat
            ):
                continue
            mdest = os.path.join(name, os.path.relpath(mdir, srcpath))
            # Check for symlinks that happen to point to an empty dir.
            if keep_symlinks:
                for link in symlinks:
                    if mdir.startswith(link, 0):
                        log.debug(
                            "** skipping empty dir ** {0}, it intersects"
                            " a symlink".format(mdir)
                        )
                    continue
            manage_directory(mdest)
            keep.add(mdest)

    keep = list(keep)
    if clean:
        # TODO: Use directory(clean=True) instead
        keep += _gen_keep_files(name, require)
        removed = _clean_dir(name, list(keep), exclude_pat)
        if removed:
            if __opts__["test"]:
                if ret["result"]:
                    ret["result"] = None
                add_comment("removed", removed)
            else:
                ret["changes"]["removed"] = removed

    # Flatten comments until salt command line client learns
    # to display structured comments in a readable fashion
    ret["comment"] = "\n".join(
        "\n#### {0} ####\n{1}".format(
            k, v if isinstance(v, basestring) else "\n".join(v)
        )
        for (k, v) in ret["comment"].iteritems()
    ).strip()

    if not ret["comment"]:
        ret["comment"] = "Recursively updated {0}".format(name)

    if not ret["changes"] and ret["result"]:
        ret["comment"] = "The directory {0} is in the correct state".format(name)

    return ret