///|
priv suberror TreeError {
TreeError(String)
}
///|
priv struct TreeOptions {
all : Bool
directories_only : Bool
full_path : Bool
level : Int?
no_report : Bool
help : Bool
version : Bool
}
///|
priv struct TreeInvocation {
options : TreeOptions
roots : Array[String]
}
///|
priv struct TreeNode {
name : String
path : String
kind : @fs.FileKind
children : Array[TreeNode]
}
///|
priv struct TreeCounts {
directories : Int
files : Int
}
///|
fn join_path(parent : String, child : String) -> String {
if parent == "/" {
"/" + child
} else if parent == "." {
"./" + child
} else if parent == "" {
child
} else {
parent + "/" + child
}
}
///|
fn trim_trailing_slashes(path : String) -> String {
if path == "" {
return path
}
let chars : Array[Char] = path.iter().collect()
let mut end = chars.length()
while end > 1 && chars[end - 1] == '/' {
end -= 1
}
let out = StringBuilder()
for index in 0.. String {
let trimmed = trim_trailing_slashes(path)
if trimmed == "/" {
return "/"
}
let chars : Array[Char] = trimmed.iter().collect()
let mut start = chars.length()
while start > 0 && chars[start - 1] != '/' {
start -= 1
}
let out = StringBuilder()
for index in start.. Int raise TreeError {
let level = @string.parse_int(value) catch {
_ => raise TreeError("invalid level: '\{value}'")
}
if level < 0 {
raise TreeError("invalid level: '\{value}'")
}
level
}
///|
fn parse_arguments(args : Array[String]) -> TreeInvocation raise TreeError {
let parsed = @cli.parse(args, [
@cli.flag("all", short='a'),
@cli.flag("dirs-only", short='d'),
@cli.flag("full-path", short='f'),
@cli.option("level", short='L'),
@cli.flag("noreport"),
@cli.flag("help"),
@cli.flag("version"),
]) catch {
@cli.CliError(message~, ..) => raise TreeError(message)
}
let level = match parsed.last_value("level") {
Some(value) => Some(parse_level(value))
None => None
}
let roots = if parsed.operands.is_empty() { ["."] } else { parsed.operands }
{
options: {
all: parsed.contains("all"),
directories_only: parsed.contains("dirs-only"),
full_path: parsed.contains("full-path"),
level,
no_report: parsed.contains("noreport"),
help: parsed.contains("help"),
version: parsed.contains("version"),
},
roots,
}
}
///|
fn display_root(path : String, full_path : Bool) -> String {
let trimmed = trim_trailing_slashes(path)
if full_path {
trimmed
} else {
trimmed
}
}
///|
async fn collect_node(
path : String,
display_name : String,
depth : Int,
options : TreeOptions,
) -> TreeNode raise TreeError {
let kind = @fs.kind(path, follow_symlink=false) catch {
_ => raise TreeError("cannot access '\{path}'")
}
if kind == SymLink {
raise TreeError(
"symbolic links are outside the supported profile: '\{path}'",
)
}
let children : Array[TreeNode] = []
let can_descend = match options.level {
Some(limit) => depth < limit
None => true
}
if kind == Directory && can_descend {
let entries = @fs.readdir(
path,
include_hidden=options.all,
include_special=true,
sort=true,
) catch {
_ => raise TreeError("cannot read directory '\{path}'")
}
for entry in entries {
let entry_path = join_path(path, entry)
let entry_kind = @fs.kind(entry_path, follow_symlink=false) catch {
_ => raise TreeError("cannot access '\{entry_path}'")
}
if entry_kind == SymLink {
raise TreeError(
"symbolic links are outside the supported profile: '\{entry_path}'",
)
}
if options.directories_only && entry_kind != Directory {
continue
}
children.push(collect_node(entry_path, entry, depth + 1, options))
}
}
{ name: display_name, path, kind, children, }
}
///|
fn node_counts(node : TreeNode) -> TreeCounts {
let mut directories = if node.kind == Directory { 1 } else { 0 }
let mut files = if node.kind == Directory { 0 } else { 1 }
for child in node.children {
let child_counts = node_counts(child)
directories += child_counts.directories
files += child_counts.files
}
{ directories, files, }
}
///|
fn node_label(node : TreeNode, options : TreeOptions, is_root : Bool) -> String {
if is_root {
display_root(node.path, options.full_path)
} else if options.full_path {
node.path
} else {
node.name
}
}
///|
fn render_node(
node : TreeNode,
options : TreeOptions,
prefix : String,
is_last : Bool,
is_root : Bool,
output : StringBuilder,
) -> Unit {
if is_root {
output.write_string(node_label(node, options, true))
output.write_char('\n')
} else {
output.write_string(prefix)
output.write_string(if is_last { "└── " } else { "├── " })
output.write_string(node_label(node, options, false))
output.write_char('\n')
}
let child_prefix = if is_root {
""
} else if is_last {
prefix + " "
} else {
prefix + "│ "
}
for index, child in node.children {
render_node(
child,
options,
child_prefix,
index == node.children.length() - 1,
false,
output,
)
}
}
///|
fn write_report(counts : TreeCounts, output : StringBuilder) -> Unit {
let directory_word = if counts.directories == 1 {
"directory"
} else {
"directories"
}
let file_word = if counts.files == 1 { "file" } else { "files" }
output.write_string(
"\{counts.directories} \{directory_word}, \{counts.files} \{file_word}\n",
)
}
///|
fn add_counts(left : TreeCounts, right : TreeCounts) -> TreeCounts {
{
directories: left.directories + right.directories,
files: left.files + right.files,
}
}
///|
fn usage() -> String {
"Usage: tree [-adf] [-L level] [--noreport] [--] [directory ...]\n"
}
///|
async fn main {
let invocation = parse_arguments(@env.args()[1:].to_owned()) catch {
TreeError(message) => {
@stdio.stderr.write("tree: \{message}\n")
@sys.exit(2)
return
}
}
if invocation.options.help {
@stdio.stdout.write(usage())
return
}
if invocation.options.version {
@stdio.stdout.write("tree (moonbit cmd) \{command_version}\n")
return
}
let roots : Array[TreeNode] = []
let mut failed = false
for root in invocation.roots {
let path = trim_trailing_slashes(root)
let display_name = basename(path)
let node = collect_node(path, display_name, 0, invocation.options) catch {
TreeError(message) => {
@stdio.stderr.write("tree: \{message}\n")
failed = true
continue
}
}
roots.push(node)
}
if failed {
@sys.exit(1)
return
}
let output = StringBuilder()
let mut counts : TreeCounts = { directories: 0, files: 0, }
for index, root in roots {
if index > 0 {
output.write_char('\n')
}
render_node(root, invocation.options, "", true, true, output)
counts = add_counts(counts, node_counts(root))
}
if !invocation.options.no_report {
write_report(counts, output)
}
@stdio.stdout.write(output.to_string())
}