Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go目录树打印

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

作者:叹一曲当时只道是寻常

在日常开发中,我们经常需要可视化项目的目录结构,下面小编将介绍一款用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目录树打印内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文