Linux下有一个命令叫做tree(其实Windows也自带了一个类似的),可以生成一个目录的树形结构。

这个命令在学习软件架构,撰写文档方面挺有帮助的。今天,我们来用Python实现一个类似的函数,然后加一些包装,做成一个SublimeText3的插件。

我们插件在条目的排列上要支持一下选项Directory First

Directory Only

File First

Ordered

还要支持缩进级别参数、是否显示隐藏文件、是否显示文件尺寸(或文件夹中条目的个数),支持在条目间插入空行等待。

我们使用4个Unicode的制表符"│","├","└"和"─"来使生成的目录树看上去更美观一些。

我们使用Python的类来接受这些参数

class Tree():
mode_descriptions = {
'df': 'Directory First',
'do': 'Directory Only',
'ff': 'File First',
'od': 'Ordered'
}
def __init__(self, path, indent=4, mode='ff', layer=0,
sparse=True, dtail='/', show_hidden=False, show_size=False,
show_absolute_path_of_rootdir=False):
indent = min(max(indent, 1), 8)
self.sparse = sparse
self.dtail = dtail
self.layer = layer if layer > 0 else 65535
self.indent_space = ' ' * indent
self.down_space = '│' + ' ' * (indent - 1)
self.vert_horiz = '├' + '─' * (indent - 1)
self.turn_horiz = '└' + '─' * (indent - 1)
self.traverses = {
'df': self.df,
'do': self.do,
'ff': self.ff,
'od': self.od
}
self.listdir = os.listdir if show_hidden else listdir_nohidden
self.show_size = show_size
self.show_absolute_path_of_rootdir = show_absolute_path_of_rootdir
self.chmod(mode)
self.generate(path)

现在,我们来实现generate函数,它生成参数path对应的目录树。

def generate(self, path):
"""metadata: [(path, isfile?, size) or None], maybe use to open filesize: file size, or number of files in a Directory, is a string"""
assert os.path.isdir(path)
self.metadata = []
self.lines = [path]
if not self.show_absolute_path_of_rootdir:
self.lines[0] = os.path.basename(path)
self.traverse(path, '', 0)
if self.lines[-1] == '':
self.lines.pop()
self.metadata.pop()
if self.show_size:
sep = self.indent_space
size_len = max(len(md[2]) for md in self.metadata if md)
for i, mdata in enumerate(self.metadata):
size = mdata[2] if mdata else ' '
size = '%*s' % (size_len, size)
if self.lines[i]:
self.lines[i] = size + sep + self.lines[i]
self.tree = '\n'.join(self.lines) + '\n'

接着,为了方便后面遍历目录时保存数据,我们来写三个辅助函数

def get_dirs_files(self, dirpath):
dirs, files = [], []
for leaf in self.listdir(dirpath):
path = os.path.join(dirpath, leaf)
if os.path.isfile(path):
files.append((leaf, path))
else:
dirs.append((leaf, path))
self.metadata.append((dirpath, False, str(len(dirs) + len(files))))
return dirs, files
def add_dirs(self, dirs, prefix, recursive, layer):
fprefix = prefix + self.down_space
dprefix = prefix + self.vert_horiz
for dirname, path in dirs[:-1]:
self.lines.append(dprefix + dirname + self.dtail)
recursive(path, fprefix, layer + 1)
fprefix = prefix + self.indent_space
dprefix = prefix + self.turn_horiz
dirname, path = dirs[-1]
self.lines.append(dprefix + dirname + self.dtail)
recursive(path, fprefix, layer + 1)
def add_files(self, files, fprefix):
for filename, path in files:
size = str_size(os.path.getsize(path))
self.lines.append(fprefix + filename)
self.metadata.append((path, True, size))
if self.sparse and files:
self.lines.append(fprefix.rstrip())
self.metadata.append(None)

再接着,实现4种不同的遍历模式的函数

def df(self, dirpath, prefix, layer):
dirs, files = self.get_dirs_files(dirpath)
if layer < self.layer:
if dirs:
self.add_dirs(dirs, prefix, self.df, layer)
self.add_files(files, prefix + self.indent_space)
def do(self, dirpath, prefix, layer):
dirs, files = self.get_dirs_files(dirpath)
if layer < self.layer and dirs:
self.add_dirs(dirs, prefix, self.do, layer)
def ff(self, dirpath, prefix, layer):
dirs, files = self.get_dirs_files(dirpath)
if layer < self.layer:
if dirs:
self.add_files(files, prefix + self.down_space)
self.add_dirs(dirs, prefix, self.ff, layer)
else:
self.add_files(files, prefix + self.indent_space)
def od(self, dirpath, prefix, layer):
def add_leaf(leaf):
path = os.path.join(dirpath, leaf)
if os.path.isfile(path):
size = str_size(os.path.getsize(path))
self.lines.append(dprefix + leaf)
self.metadata.append((path, True, size))
if os.path.isdir(path):
self.lines.append(dprefix + leaf + self.dtail)
self.od(path, fprefix, layer + 1)
leaves = list(self.listdir(dirpath))
self.metadata.append((dirpath, False, str(len(leaves))))
if layer < self.layer and leaves:
leaves.sort()
fprefix = prefix + self.down_space
dprefix = prefix + self.vert_horiz
for leaf in leaves[:-1]:
add_leaf(leaf)
fprefix = prefix + self.indent_space
dprefix = prefix + self.turn_horiz
add_leaf(leaves[-1])

至此,目录树函数tree终于成功编写了。让我们把上面的代码连接起来,保存为tree.py文件。新建一个叫做SublimeDirectoryTree的目录,把tree.py拖进去,再在该目录下新建一个叫做Side Bar.py或main.py的文件,在里面写入如下代码(这个代码是有固定的格式的喔)

import os
import sublime
import sublime_plugin
from .tree import Tree
class SidebarMakeTreeCommand(sublime_plugin.WindowCommand):
def get_tree_settings(self):
settings = sublime.load_settings(
'SublimeDirectoryTree.sublime-settings')
tree_settings = settings.get('args', { "dir_tail_character": "/" })
tree_settings['dtail'] = tree_settings.pop("dir_tail_character")
return tree_settings
def is_visible(self, paths):
return len(paths) == 1 and os.path.isdir(paths[0])
def run(self, paths):
tree_settings = self.get_tree_settings()
tree = Tree(paths[0], **tree_settings)
text = "mode:%s\n\n" % tree.mode
text += tree.tree
view = self.window.new_file()
view.assign_syntax("tree.sublime-syntax")
view.set_name("%s.tr" % os.path.basename(paths[0]))
view.settings().set("font_face", "Lucida Console")
view.settings().set("word_wrap", False)
view.run_command("append", {"characters": text})
view.set_scratch(True)
view.set_read_only(True)
class SidebarCopyTreeCommand(SidebarMakeTreeCommand):
def run(self, paths):
tree_settings = self.get_tree_settings()
tree = Tree(paths[0], **tree_settings)
sublime.set_clipboard(tree.tree)

这样,我们就成功开发了一个SublimeText3编辑器的插件。当然,为了让我们的插件使用起来更加方便,我们还需要为它添加一些菜单:

[
{"caption": "-", "id": "folder_menus"},
{
"caption": "Copy Directory Tree",
"command": "sidebar_copy_tree",
"args": {"paths": []}
},
{
"caption": "View Directory Tree",
"command": "sidebar_make_tree",
"args": {"paths": []}
},
]

在同目录下保存为Side Bar.sublime-menu。

???这不就是Python的字典吗?

不,你错了,它只是长得像Python的字典,实际上它是JSON数据文件格式。

喔对了,上面不是说我们的插件支持一些选项吗?给忘了,让我们来添加一个设置文件吧

{
/*
* mode explain:
* df: Directory First,
* do: Directory Only,
* ff: File First,
* od: Ordered
*/
"args": {
"mode": "ff",
"layer": 3,
"indent": 4,
"sparse": false,
"show_size": false,
"show_hidden": false,
"dir_tail_character": "/",
"show_absolute_path_of_rootdir": false
}
}

在同目录下保存为SublimeDirectoryTree.sublime-settings。

把SublimeDirectoryTree文件夹整个复制到SublimeText的插件目录下,我们就可以使用自己的插件了。

整个流程下来,你是不是发现开发一个能够实用的SublimeText3插件是如此的简单。

完整的插件位于absop/SublimeDirectoryTreegithub.com

有兴趣的同学可以参考参考。