递归列出时如何知道最后一个文件或目录?

时间:2019-04-08 16:22:40

标签: c# recursion io .net-standard

简介

我正在编写DirectoryTreeDrawer类随附的库。它的主要目的是根据提供的目录路径或TextWriter实例绘制树结构(到基础DirectoryInfo)。

演示

以下是使用DirectoryTreeDrawer类的示例.NET Core控制台应用程序:

public class Program
{
    public static void Main()
    {
        using (var drawer = new DirectoryTreeDrawer(System.Console.Out))
        {
            var workingDirectoryPath = System.IO.Directory.GetCurrentDirectory();
            drawer.DrawTree(workingDirectoryPath);
        }
    }
}

样本输出

运行上面的命令将产生类似于下面几行的输出(为简洁起见被截断):

ConsoleDemo\
├────ConsoleDemo.csproj.nuget.cache
├────ConsoleDemo.csproj.nuget.g.props
├────ConsoleDemo.csproj.nuget.g.targets
├────project.assets.json
├──ConsoleDemo.csproj
├──Program.cs

工作原理

DrawTree()调用PrintDirectoryContent(),在此开始递归魔术。从提供的路径开始,该程序递归地遍历子目录,并以反映原始目录相对深度的方式打印文件和目录的名称。

public void DrawTree(DirectoryInfo directoryInfo)
{
    var searchPattern = "*";
    var searchOption = SearchOption.TopDirectoryOnly;

    PrintDirectoryName(directoryInfo, depth: 0);
    PrintDirectoryContent(directoryInfo, searchPattern, searchOption, depth: 0);
}

private void PrintDirectoryContent(DirectoryInfo currentDirectory, string searchPattern, SearchOption searchOption, int depth)
{
    var directories = currentDirectory.GetDirectories(searchPattern, searchOption);
    var directoriesCount = directories.GetLength(0);
    for (var directoryIndex = 0; directoryIndex < directoriesCount; directoryIndex++)
    {
        var directoryInfo = directories[directoryIndex];
        PrintDirectoryName(directoryInfo, depth + 1);
        PrintDirectoryContent(directoryInfo, searchPattern, searchOption, depth + 1);
    }

    var files = currentDirectory.GetFiles(searchPattern, searchOption);
    var filesCount = files.GetLength(0);
    for (var fileIndex = 0; fileIndex < filesCount; fileIndex++)
    {
        var fileInfo = files[fileIndex];
        PrintFileName(fileInfo, depth + 1);
    }
}

辅助方法

文件(或目录)前缀由单个符号组成,后跟分别重复到当前深度的符号。

private void PrintDirectoryName(DirectoryInfo directoryInfo, int depth)
{
    _textWriter.WriteLine($"{CreateDepthPrefix(depth)}{directoryInfo.Name}{Path.DirectorySeparatorChar}");
}

private void PrintFileName(FileInfo fileInfo, int depth)
{
    _textWriter.WriteLine($"{CreateDepthPrefix(depth)}{fileInfo.Name}");
}

private string CreateDepthPrefix(int depth)
{
    return $"{'├'}{new string('─', 2 * depth)}";
}

问题

我想用与通常的前缀不同的前缀标记最后一个条目(文件或目录)。我不想以符号开头,而是以符号开头。所以输出的最后一行代替:

├──Program.cs

...看起来像这样:

└──Program.cs

对我来说,问题在于如何知道哪个文件或目录是最后一个要打印的文件。如果我知道,可以在打印前缀时简单地进行检查。

是否有更好的解决方案,然后将所有条目(文件和目录的名称和深度)保存到集合中,然后对“ being-the-last-entry”条件进行检查?或者也许是唯一的一个?

代码存储库

该库是开放源代码,可在GitLab上使用。在这里,您还可以找到原始的DirectoryTreeDrawer类。请注意,出于代码简洁的目的,我对其进行了大量编辑。

最终通知

我想说明的是,我并没有要求对代码进行审查,就像有人看起来那样。我遇到了问题,正在寻求解决方案。

3 个答案:

答案 0 :(得分:1)

我的简短回答是我在评论中写的内容,彼得已经提供了答案,但这是另一种格式,可能被认为更具可读性,它为每个目录和文件提供缩进,因此更容易看看它属于哪个父母。请参阅最后一张图片以获取示例输出。

这是通过跟踪父级文件中的最后一个文件或文件夹并将其传递给PrintItem方法来完成的(这是我在对问题的评论中提出的答案)。另一个更改是前缀从父级传递到子级,因此我们可以包括嵌套项目的连接器。为了跟踪“嵌套”的项(意味着该项的父项具有在当前项之后显示的同级项),我们将一个IsNested参数传递给PrintDirectory方法,以便前缀可以进行相应的更新。

我也将其修改为static类,将TextWriter传递给不同的方法。不知道这是否真的更好,但是除了TextWriter之外,其他所有内容似乎都应该是静态的。

public static class DirectoryTreeDrawer
{
    public static void DrawTree(string directoryPath, TextWriter textWriter)
    {
        DrawTree(new DirectoryInfo(directoryPath), textWriter);
    }

    public static void DrawTree(DirectoryInfo directoryInfo, TextWriter textWriter)
    {
        PrintDirectory(directoryInfo, textWriter);
    }

    private static void PrintDirectory(DirectoryInfo directory, TextWriter textWriter, 
        string prefix = "  ", string searchPattern = "*", SearchOption searchOption = 
        SearchOption.TopDirectoryOnly, bool isLast = true, bool isNested = false)
    {
        PrintItem(directory.Name, prefix, isLast, textWriter, true);

        var subDirs = directory.GetDirectories(searchPattern, searchOption);
        var files = directory.GetFiles(searchPattern, searchOption);

        // If this is a "nested" directory, add the parent's connector to the prefix
        prefix += isNested ? "│ " : "  ";

        for (var directoryIndex = 0; directoryIndex < subDirs.Length; directoryIndex++)
        {
            var isLastChild = directoryIndex == subDirs.Length - 1 && files.Length == 0;

            // If the parent has files or other directories, mark this as "nested"
            var isNestedDir = files.Length > 0 || !isLastChild;

            PrintDirectory(subDirs[directoryIndex], textWriter, prefix, searchPattern, 
                searchOption, isLastChild, isNestedDir);
        }            

        for (var fileIndex = 0; fileIndex < files.Length; fileIndex++)
        {
            var isLastFile = fileIndex == files.Length - 1;

            PrintItem(files[fileIndex].Name, prefix, isLastFile, textWriter);
        }
    }

    private static void PrintItem(string name, string prefix, bool isLastItem, 
        TextWriter textWriter, bool isDirectory = false)
    {
        var itemConnector = isLastItem ? "└─" : "├─";
        var suffix = isDirectory ? Path.DirectorySeparatorChar.ToString() : "";

        textWriter?.WriteLine($"{prefix}{itemConnector}{name}{suffix}");
    }
}

用法

private static void Main()
{
    DirectoryTreeDrawer.DrawTree(Environment.CurrentDirectory, Console.Out);

    GetKeyFromUser("\nDone! Press any key to exit...");
}

输出

Sample Output

从输出来看,很明显,我多年来一直在重复使用同一项目,并且Debug文件夹中有一堆无关紧要的文件...:)

答案 1 :(得分:0)

我要做什么,我将列出所有这些内容,然后更改光标位置,然后更改打印的字符:

public void DrawTree(string directoryPath)
        {
            if (string.IsNullOrWhiteSpace(directoryPath))
            {
                throw new ArgumentException(
                    "Provided directory path is null, emtpy " +
                    "or consists of only whitespace characters.",
                    nameof(directoryPath));
            }

            DrawTree(new DirectoryInfo(directoryPath));
            //remember current position because we need to return position to it
            int currentCursorPositionTop = Console.CursorTop;
            //set position to the last row
            Console.SetCursorPosition(0, Console.CursorTop-1);
            //change the first charachter
            Console.Write("└");
            //return cursor position to the previous one so our "Press any key to continue" can apear below our list.
            Console.SetCursorPosition(0, currentCursorPositionTop);
        }

我希望这会有所帮助。 这是应用更改时的样子: enter image description here

更新: 为了维持更高的抽象水平,我建议将数据写入 列出然后更改最后一个的前缀:

class ContentItem {
    public string Prefix {get;set; }
    public int Depth {get;set; }
    public string Name {get;set; }
    public override string ToString() {
        return $"{Prefix}{(new String("-", Depth))} {Name}";
    }
}

因此,可以更改列表项前缀来代替更改控制台光标的位置:

 var items[items.Count()-1].Prefix = "└";

,然后循环遍历这些项目,并将其传递给TextWriter,StreamWriter或其他任何对象。

答案 2 :(得分:0)

  

问题在于如何知道哪个文件或目录是最后一个要打印的文件。如果我知道,可以在打印前缀时简单地进行检查。

鉴于您发布的代码,知道您是否在最后一个条目很简单,因为您的递归方法具有一个depth参数,并且为每个项目的循环编制了索引。这意味着您只需要查看自己的int值,例如:

private void PrintDirectoryContent(DirectoryInfo currentDirectory, string searchPattern, SearchOption searchOption, int depth)
{
    var directories = currentDirectory.GetDirectories(searchPattern, searchOption);
    var directoriesCount = directories.GetLength(0);
    for (var directoryIndex = 0; directoryIndex < directoriesCount; directoryIndex++)
    {
        var directoryInfo = directories[directoryIndex];
        PrintDirectoryName(directoryInfo, depth + 1);
        PrintDirectoryContent(directoryInfo, searchPattern, searchOption, depth + 1);
    }

    var files = currentDirectory.GetFiles(searchPattern, searchOption);
    var filesCount = files.GetLength(0);
    for (var fileIndex = 0; fileIndex < filesCount; fileIndex++)
    {
        var fileInfo = files[fileIndex];
        PrintFileName(fileInfo, depth + 1, depth == 0 && fileIndex == filesCount - 1);
    }
}

private void PrintDirectoryName(DirectoryInfo directoryInfo, int depth)
{
    _textWriter.WriteLine($"{CreateDepthPrefix('├', depth)}{directoryInfo.Name}{Path.DirectorySeparatorChar}");
}

private void PrintFileName(FileInfo fileInfo, int depth, bool isLast)
{
    _textWriter.WriteLine($"{CreateDepthPrefix(isLast ? '└' : '├', depth)}{fileInfo.Name}");
}

private string CreateDepthPrefix(char initialChar, int depth)
{
    return $"{initialChar}{new string('─', 2 * depth)}";
}

即表达式depth == 0 && fileIndex == filesCount - 1就是我添加到isLast方法中的PrintFileName()参数的确切值。

请注意,由于缺乏良好的Minimal, Complete, and Verifiable example,我不得不对您发布的代码进行一些调整,以使其能够编译和运行。您发布的代码也不会产生您的问题所说明的输出;具体来说,顶级目录名称也以'├'字符结尾。我猜在您的实际代码中,该行是特例。

我没有花任何时间试图使输出与您所说的完全匹配,而是宁愿只关注眼前的问题。我假设您可以将上面的代码(包含该问题的答案)改编为实际使用的任何代码。