基于Go语言实现一个目录树打印工具

 更新时间:2025年06月17日 08:28:49   作者:叹一曲当时只道是寻常  
在日常开发中,我们经常需要可视化项目的目录结构,下面小编将介绍一款用Go语言开发的目录树打印工具,它不仅能生成美观的目录结构图,还提供多种实用功能

在日常开发中,我们经常需要可视化项目的目录结构。无论是编写文档、分享项目结构,还是单纯了解代码布局,一个清晰的目录树展示都至关重要。今天我将介绍一款用Go语言开发的目录树打印工具,它不仅能生成美观的目录结构图,还提供多种实用功能!

功能亮点

多层级展示:支持自定义深度限制

隐藏文件处理:可选显示隐藏文件/文件夹

多种输出方式:控制台打印、保存文件、复制到剪贴板

美观可视化:使用emoji图标标识不同类型

灵活连接线:可选的树形连接线展示

智能排序:目录优先,按名称排序

技术实现解析

核心数据结构

type DirectoryPrinter struct {
    rootDir       string      // 根目录路径
    showHidden    bool        // 是否显示隐藏文件
    currentDepth  int         // 当前递归深度
    maxDepth      int         // 最大深度限制
    indentSymbol  string      // 缩进符号(4个空格)
    folderSymbol  string      // 文件夹图标
    fileSymbol    string      // 文件图标
    output        []string    // 输出内容收集
    showConnector bool        // 是否显示连接线
}

关键算法逻辑

1.文件排序策略:

目录优先于文件

相同类型按名称升序排列

sort.Slice(filteredEntries, func(i, j int) bool {
    if filteredEntries[i].IsDir() != filteredEntries[j].IsDir() {
        return filteredEntries[i].IsDir()
    }
    return filteredEntries[i].Name() < filteredEntries[j].Name()
})

2.连接线生成逻辑:

非最后一项:├──

最后一项:└──

if dp.showConnector {
    isLast := index == total-1
    connector := "├── "
    if isLast {
        connector = "└── "
    }
    return fmt.Sprintf("%s%s📁 %s\n", prefix, connector, entry.Name())
}

3.递归深度控制:

if dp.maxDepth > 0 && dp.currentDepth >= dp.maxDepth {
    return nil
}

使用指南

命令行参数

参数说明示例
--show-hidden显示隐藏文件--show-hidden
--output-file保存到文件--output-file tree.txt
--copy-ClipBoard复制到剪贴板--copy-ClipBoard
--max-depth设置最大深度--max-depth 3
--show-connector显示连接线--show-connector
--help显示帮助--help

使用示例

基本用法(显示当前目录结构):

directory_printer

显示隐藏文件并限制深度:

directory_printer --show-hidden --max-depth 2

保存到文件并显示连接线:

directory_printer --output-file project_tree.txt --show-connector

输出示例

📂 my-project
    📁 src
        📁 controllers
        📁 models
        📁 views
    📁 config
    📁 public
        📁 css
        📁 js
        📁 images
    📄 README.md
    📄 .gitignore
    📄 go.mod

带连接线版本:

📂 my-project
├── 📁 src
│   ├── 📁 controllers
│   ├── 📁 models
│   └── 📁 views
├── 📁 config
├── 📁 public
│   ├── 📁 css
│   ├── 📁 js
│   └── 📁 images
├── 📄 README.md
├── 📄 .gitignore
└── 📄 go.mod

实现细节解析

根目录处理

rootName := filepath.Base(rootDir)
if rootName == "." {
    // 获取当前工作目录的绝对路径
    absPath, err := filepath.Abs(rootDir)
    if err != nil {
        rootName = "current_directory"
    } else {
        rootName = filepath.Base(absPath)
    }
}
printer.output = append(printer.output, fmt.Sprintf("📂 %s\n", rootName))

输出处理逻辑

// 保存到文件
if *outputFile != "" {
    err = saveToFile(*outputFile, printer.output)
}

// 复制到剪贴板
if *copyClipBoard {
    content := strings.Join(printer.output, "")
    err = clipboard.WriteAll(content)
}

​​​​​​​// 控制台输出
fmt.Println("\n=== Directory Structure ===")
for _, line := range printer.output {
    fmt.Print(line)
}

递归目录遍历

childPrinter := &DirectoryPrinter{
    rootDir:       filepath.Join(dp.rootDir, entry.Name()),
    showHidden:    dp.showHidden,
    currentDepth:  dp.currentDepth + 1,
    maxDepth:      dp.maxDepth,
    indentSymbol:  dp.indentSymbol,
    folderSymbol:  dp.folderSymbol,
    fileSymbol:    dp.fileSymbol,
    output:        dp.output,
    showConnector: dp.showConnector,
}
if err := childPrinter.printDirectory(); err != nil {
    return err
}
dp.output = childPrinter.output

附录

完整代码

package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/atotto/clipboard"
)

type DirectoryPrinter struct {
	rootDir       string
	showHidden    bool
	currentDepth  int
	maxDepth      int
	indentSymbol  string
	folderSymbol  string
	fileSymbol    string
	output        []string
	showConnector bool // 新增字段控制是否显示连接线
}

func (dp *DirectoryPrinter) printDirectory() error {
	// 检查是否超过最大深度
	if dp.maxDepth > 0 && dp.currentDepth >= dp.maxDepth {
		return nil
	}

	entries, err := os.ReadDir(dp.rootDir)
	if err != nil {
		return err
	}

	// 过滤隐藏文件
	var filteredEntries []os.DirEntry
	for _, entry := range entries {
		if dp.showHidden || !strings.HasPrefix(entry.Name(), ".") {
			filteredEntries = append(filteredEntries, entry)
		}
	}

	// 按类型(目录优先)和名称排序
	sort.Slice(filteredEntries, func(i, j int) bool {
		// 首先按类型排序(目录在前)
		if filteredEntries[i].IsDir() != filteredEntries[j].IsDir() {
			return filteredEntries[i].IsDir()
		}
		// 同类型按名称排序
		return filteredEntries[i].Name() < filteredEntries[j].Name()
	})

	total := len(filteredEntries)
	for i, entry := range filteredEntries {
		prefix := strings.Repeat(dp.indentSymbol, dp.currentDepth)
		var line string
		if entry.IsDir() {
			line = dp.buildFolderLine(prefix, i, total, entry)
		} else {
			line = dp.buildFileLine(prefix, i, total, entry)
		}
		dp.output = append(dp.output, line)

		// 递归处理子文件夹
		if entry.IsDir() {
			childPrinter := &DirectoryPrinter{
				rootDir:       filepath.Join(dp.rootDir, entry.Name()),
				showHidden:    dp.showHidden,
				currentDepth:  dp.currentDepth + 1,
				maxDepth:      dp.maxDepth,
				indentSymbol:  dp.indentSymbol,
				folderSymbol:  dp.folderSymbol,
				fileSymbol:    dp.fileSymbol,
				output:        dp.output,
				showConnector: dp.showConnector,
			}
			if err := childPrinter.printDirectory(); err != nil {
				return err
			}
			dp.output = childPrinter.output
		}
	}

	return nil
}

func (dp *DirectoryPrinter) buildFolderLine(prefix string, index, total int, entry os.DirEntry) string {
	if dp.showConnector {
		isLast := index == total-1
		connector := "├── "
		if isLast {
			connector = "└── "
		}
		return fmt.Sprintf("%s%s📁 %s\n", prefix, connector, entry.Name())
	}
	return fmt.Sprintf("%s📁 %s\n", prefix, entry.Name())
}

func (dp *DirectoryPrinter) buildFileLine(prefix string, index, total int, entry os.DirEntry) string {
	if dp.showConnector {
		isLast := index == total-1
		connector := "├── "
		if isLast {
			connector = "└── "
		}
		return fmt.Sprintf("%s%s📄 %s\n", prefix, connector, entry.Name())
	}
	return fmt.Sprintf("%s📄 %s\n", prefix, entry.Name())
}

func usage() {
	fmt.Println("Usage: directory_printer [OPTIONS] [PATH]")
	fmt.Println("\nOptions:")
	fmt.Println("  --show-hidden               Include hidden files and directories")
	fmt.Println("  --output-file <file_path>   Save the directory structure to a file")
	fmt.Println("  --copy-ClipBoard            Copy Directory structure to clipboard")
	fmt.Println("  --max-depth <number>        Maximum directory depth to display (0 for all levels, 1 for root only)")
	fmt.Println("  --show-connector            Show connector characters (├── and └──)")
	fmt.Println("  --help                      Display this help message")
	fmt.Println("\nExample:")
	fmt.Println("  directory_printer --show-hidden --max-depth 2 --output-file output.txt /path/to/directory")
}

func isDirectory(path string) bool {
	fileInfo, err := os.Stat(path)
	if err != nil {
		return false
	}
	return fileInfo.IsDir()
}

func saveToFile(filePath string, content []string) error {
	file, err := os.Create(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	for _, line := range content {
		if _, err := file.WriteString(line); err != nil {
			return err
		}
	}

	return nil
}

func main() {
	showHidden := flag.Bool("show-hidden", false, "Include hidden files and directories")
	outputFile := flag.String("output-file", "", "Save the directory structure to a file")
	copyClipBoard := flag.Bool("copy-ClipBoard", true, "Copy Directory structure to clipboard")
	maxDepth := flag.Int("max-depth", 0, "Maximum directory depth to display (0 for all levels, 1 for root only)")
	showConnector := flag.Bool("show-connector", false, "Show connector characters (├── and └──)")
	help := flag.Bool("help", false, "Display this help message")
	flag.Parse()

	if *help {
		usage()
		os.Exit(0)
	}

	var rootDir string
	if len(flag.Args()) == 0 {
		rootDir = "."
	} else {
		rootDir = flag.Arg(0)
	}

	if !isDirectory(rootDir) {
		fmt.Printf("Error: %s is not a valid directory\n", rootDir)
		os.Exit(1)
	}

	printer := &DirectoryPrinter{
		rootDir:       rootDir,
		showHidden:    *showHidden,
		currentDepth:  0,
		maxDepth:      *maxDepth,
		indentSymbol:  "    ", // 使用4个空格作为视觉缩进
		folderSymbol:  "",
		fileSymbol:    "",
		output:        []string{},
		showConnector: *showConnector,
	}

	rootName := filepath.Base(rootDir)
	if rootName == "." {
		// 获取当前工作目录的绝对路径
		absPath, err := filepath.Abs(rootDir)
		if err != nil {
			rootName = "current_directory"
		} else {
			rootName = filepath.Base(absPath)
		}
	}
	printer.output = append(printer.output, fmt.Sprintf("📂 %s\n", rootName))
	// 增加根目录的缩进
	printer.currentDepth = 1

	err := printer.printDirectory()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		os.Exit(1)
	}

	if *outputFile != "" {
		err = saveToFile(*outputFile, printer.output)
		if err != nil {
			fmt.Printf("Failed to save to file: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("Directory structure saved to: %s\n", *outputFile)
	}
	if *copyClipBoard {
		content := strings.Join(printer.output, "")
		err = clipboard.WriteAll(content)
		if err != nil {
			fmt.Printf("Failed to copy to clipboard: %v\n", err)
		} else {
			fmt.Println("Directory structure copied to clipboard")
		}
	}

	fmt.Println("\n=== Directory Structure ===")
	for _, line := range printer.output {
		fmt.Print(line)
	}
}

总结

这款Go语言实现的目录树打印工具通过简洁的代码实现了强大的功能,无论是开发者快速查看项目结构,还是编写技术文档时展示目录布局,它都是一个得力的助手。清晰的emoji标识、灵活的输出选项和可定制的显示深度,让它成为你开发工具箱中不可或缺的一员。

到此这篇关于基于Go语言实现一个目录树打印工具的文章就介绍到这了,更多相关Go目录树打印内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang xorm及time.Time自定义解决json日期格式的问题

    golang xorm及time.Time自定义解决json日期格式的问题

    这篇文章主要介绍了golang xorm及time.Time自定义解决json日期格式的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Golang json 库中的RawMessage功能原理

    Golang json 库中的RawMessage功能原理

    今天我们来学习一个 Golang 官方 json 库提供了一个经典能力RawMessage,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • go 打包运行文件在windows,liunx运行

    go 打包运行文件在windows,liunx运行

    这篇文章主要介绍了go 打包运行文件在windows,liunx运行的相关资料,需要的朋友可以参考下
    2023-11-11
  • GoLang channel关闭状态相关操作详解

    GoLang channel关闭状态相关操作详解

    Channel 和 goroutine 的结合是 Go 并发编程的大杀器。而 Channel 的实际应用也经常让人眼前一亮,通过与 select,cancel,timer 等结合,它能实现各种各样的功能。接下来,我们就要介绍GoLang channel关闭状态相关操作
    2022-10-10
  • gin正确多次读取http request body内容实现详解

    gin正确多次读取http request body内容实现详解

    这篇文章主要为大家介绍了gin正确多次读取http request body内容实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • golang踩坑实战之channel的正确使用方式

    golang踩坑实战之channel的正确使用方式

    Golang channel是Go语言中一个非常重要的特性,除了用来处理并发编程的任务中,它还可以用来进行消息传递和事件通知,这篇文章主要给大家介绍了关于golang踩坑实战之channel的正确使用方式,需要的朋友可以参考下
    2023-06-06
  • 详解Go语言如何利用上下文进行并发计算

    详解Go语言如何利用上下文进行并发计算

    在Go编程中,上下文(context)是一个非常重要的概念,它包含了与请求相关的信息,本文主要来和大家讨论一下如何在并发计算中使用上下文,感兴趣的可以了解下
    2024-02-02
  • Golang对MongoDB数据库的操作简单封装教程

    Golang对MongoDB数据库的操作简单封装教程

    mongodb官方没有关于go的mongodb的驱动,因此只能使用第三方驱动,mgo就是使用最多的一种。下面这篇文章主要给大家介绍了关于利用Golang对MongoDB数据库的操作简单封装的相关资料,需要的朋友可以参考下
    2018-07-07
  • Golang设计模式之组合模式讲解

    Golang设计模式之组合模式讲解

    这篇文章主要介绍了Golang设计模式之组合模式,组合模式针对于特定场景,如文件管理、组织管理等,使用该模式能简化管理,使代码变得非常简洁
    2023-01-01
  • golang channel多协程通信常用方法底层原理全面解析

    golang channel多协程通信常用方法底层原理全面解析

    channel 是 goroutine 与 goroutine 之间通信的重要桥梁,借助 channel,我们能很轻易的写出一个多协程通信程序,今天,我们就来看看这个 channel 的常用用法以及底层原理
    2023-09-09

最新评论