search.nct
see ./index.nct
use std/cli.Application
use std/fs.FileType
use std/fs
use std/io.{BlockingFile, BlockingWriter}
use std/io
use std/io/buffer.{BlockingBufReader, BlockingBufWriter}
use std/string.String
use std/vec.Vec
instance SearchPath {
operator (&self == other: &Self): bool {
return &self.display == &other.display
}
operator (&self < other: &Self): bool {
return &self.display < &other.display
}
}
blocking func run(): i32 {
var application = Application.new("text-search", "Search a directory tree for matching text.") catch failure {
report_failure(&failure) catch _ { return 2 }
return 2
}
application.add_positional("NEEDLE", true, "Substring to search for.") catch failure {
report_failure(&failure) catch _ { return 2 }
return 2
}
application.add_positional("ROOT", true, "Directory tree to search.") catch failure {
report_failure(&failure) catch _ { return 2 }
return 2
}
let arguments = application.parse_process() catch failure {
if failure.has_code("std.cli.missing_positional")
|| failure.has_code("std.cli.unexpected_positional") {
report_usage(&application) catch _ { return 2 }
} else {
report_failure(&failure) catch _ { return 2 }
}
return 2
}
let needle = arguments.positional("NEEDLE") otherwise { return 2 }
let root = arguments.positional("ROOT") otherwise { return 2 }
let matched = search_tree(needle, root) catch failure {
report_failure(&failure) catch _ { return 2 }
return 2
}
if matched {
return 0
}
return 1
}
blocking func search_tree(needle: &str, root: &str): bool! {
var paths: Vec<SearchPath> = Vec.empty()
collect_directory(root, "", &+paths)?
paths.sort()
var output = BlockingBufWriter.new(io.stdout())
var matched = false
for path in &paths {
if search_file(&+output, needle, path)? {
matched = true
}
}
let _ = output.finish()?
return matched
}
blocking func collect_directory(
full_directory: &str,
display_directory: &str,
files: &+Vec<SearchPath>,
): void! {
var stream = fs.read_dir_blocking(full_directory) catch failure {
let context = String.concat("cannot read directory ", full_directory)
return failure.context(&context)
}
var directories: Vec<SearchPath> = Vec.empty()
while true {
let entry = stream.next_blocking() catch failure {
let context = String.concat("cannot continue directory ", full_directory)
return failure.context(&context)
} otherwise {
break
}
let name = entry.file_name()
let full: &str = entry.path()
let display = display_child(display_directory, name)
if entry.file_type() is FileType.regular {
files.push(
SearchPath {
full: String.copy(full),
display: move display,
},
)
} else if entry.file_type() is FileType.directory {
directories.push(
SearchPath {
full: String.copy(full),
display: move display,
},
)
}
}
stream.close()
directories.sort()
for directory in move directories {
collect_directory(&directory.full, &directory.display, files)?
}
return
}
func display_child(parent: &str, name: &str): String {
if parent.is_empty() {
return String.copy(name)
}
return String.concat(parent, "/", name)
}
blocking func search_file(
output: &+BlockingBufWriter<BlockingFile>,
needle: &str,
path: &SearchPath,
): bool! {
let source = BlockingFile.open(&path.full) catch failure {
let context = String.concat("cannot open ", &path.display)
return failure.context(&context)
}
var reader = BlockingBufReader.new(move source)
var line = String.empty()
var line_number: usize = 1
var matched = false
while true {
let present = reader.read_line_into_blocking(&+line) catch failure {
let context = String.concat("cannot read ", &path.display)
return failure.context(&context)
}
if !present {
break
}
if line.contains(needle) {
write_match(output, &path.display, line_number, &line)?
matched = true
}
line_number += 1
}
return matched
}
blocking func write_match(
output: &+BlockingBufWriter<BlockingFile>,
path: &str,
line_number: usize,
line: &str,
): void! {
let number = line_number.to_string()
output.write_text_blocking(path)?
output.write_text_blocking(":")?
output.write_text_blocking(&number)?
output.write_text_blocking(":")?
output.write_text_blocking(line)?
output.write_text_blocking("\n")?
return
}
blocking func report_usage(application: &Application): void! {
var output = io.stderr()
let usage = application.usage()
output.write_text_blocking(&usage)?
output.write_text_blocking("\n")?
return
}
blocking func report_failure(failure: &error): void! {
var output = io.stderr()
output.write_text_blocking("text-search: ")?
output.write_text_blocking(failure.code())?
output.write_text_blocking(": ")?
output.write_text_blocking(failure.message())?
output.write_text_blocking("\n")?
return
}