///|
/// Entry point: TokeiMBT — a tokei port in MoonBit.
fn main {
let argv = @env.args()
let opts = parse_args(argv)
if opts.show_help {
println(usage())
return
}
if opts.show_version {
println("TokeiMBT " + VERSION)
return
}
if opts.list_langs {
println(@lang.language_list())
return
}
match opts.format {
Table => ()
_ => {
if opts.num_format_explicit {
fail("--num-format cannot be used with serialized output")
}
if opts.columns_explicit {
fail("--columns cannot be used with serialized output")
}
}
}
let roots : Array[String] = if opts.paths.length() > 0 {
opts.paths
} else if opts.input_file is Some(_) {
[]
} else {
["."]
}
// 1. discover files
let wopt = @walk.WalkOptions::new()
wopt.hidden = opts.hidden
wopt.respect_ignore = !opts.no_ignore
wopt.respect_dot_ignore = !opts.no_ignore_dot
wopt.respect_vcs_ignore = !opts.no_ignore_vcs
let all_files : Array[@walk.Found] = []
for root in roots {
if @fs.path_exists(root) {
for f in @walk.collect(root, wopt) {
all_files.push(f)
}
} else {
println("[tokei error]: No such file or directory: " + root)
}
}
// 2. user excludes
let files = @walk.apply_excludes(all_files, opts.excludes)
// 3. type filters
let filtered : Array[@walk.Found] = []
for f in files {
if type_allowed(opts, f.lang.key) {
filtered.push(f)
}
}
// 4. count
let counts : Map[String, @counter.Counts] = Map([])
let file_counts : Array[(String, String, @counter.Counts)] = []
for f in filtered {
match (Ok(@fs.read_file_to_bytes(f.path)) catch { e => Err(e) }) {
Ok(bytes) =>
if !is_binary(bytes) {
let src = @counter.utf8_decode(bytes)
let c = @counter.count_source(f.lang, src)
file_counts.push((f.path, f.lang.key, c))
let key = f.lang.key
match counts.get(key) {
Some(acc) => acc.add(c)
None => counts.set(key, c.copy())
}
}
Err(@fs.IOError(msg)) =>
println("[tokei warning]: could not read " + f.rel + ": " + msg)
}
}
match opts.input_file {
Some(path) =>
match (Ok(@fs.read_file_to_string(path)) catch { e => Err(e) }) {
Ok(content) =>
match @saved.parse(content) {
Some(snapshot) => {
for kv in snapshot.counts {
match counts.get(kv.0) {
Some(existing) => existing.add(kv.1)
None => counts.set(kv.0, kv.1)
}
}
for entry in snapshot.files {
file_counts.push(entry)
}
}
None => fail("cannot parse saved Tokei JSON report: " + path)
}
Err(@fs.IOError(msg)) => fail("cannot read input " + path + ": " + msg)
}
None => ()
}
// 5. report & output
let rep = @report.build(counts, opts.sort_key, opts.reverse)
match opts.format {
Table =>
if opts.list_files {
emit(
opts,
@output.render_files_with_options(
rep,
file_counts,
opts.columns,
opts.num_format,
opts.compact,
),
)
} else {
emit(
opts,
@output.render_table_with_files(
rep,
file_counts,
opts.columns,
opts.num_format,
opts.compact,
),
)
}
Json => emit(opts, @output.render_json(rep, file_counts))
Csv => emit(opts, @output.render_csv(rep))
Yaml => emit(opts, @output.render_yaml(rep, file_counts))
}
}
///|
fn type_allowed(opts : Opts, key : String) -> Bool {
if opts.types.length() > 0 {
let mut ok = false
for t in opts.types {
if @lang.key_eq(t, key) {
ok = true
break
}
}
if !ok {
return false
}
}
if opts.types_not.length() > 0 {
for t in opts.types_not {
if @lang.key_eq(t, key) {
return false
}
}
}
true
}
///|
fn emit(opts : Opts, text : String) -> Unit {
match opts.out_file {
Some(path) =>
match (Ok(@fs.write_string_to_file(path, text)) catch { e => Err(e) }) {
Ok(_) => ()
Err(@fs.IOError(msg)) =>
println("[tokei error]: cannot write to " + path + ": " + msg)
}
None =>
if text.length() > 0 && text.at(text.length() - 1).to_int() == 0x0A {
println(text[:text.length() - 1].to_owned())
} else {
println(text)
}
}
}
///|
fn is_binary(bytes : Bytes) -> Bool {
let limit = if bytes.length() < 8192 { bytes.length() } else { 8192 }
let mut i = 0
while i < limit {
if bytes[i] == 0 {
return true
}
i += 1
}
false
}