///|
priv suberror CliError {
CliError(String)
}
///|
priv struct Counts {
mut lines : Int64
mut words : Int64
mut chars : Int64
mut bytes : Int64
mut max_line_length : Int64
}
///|
fn Counts::new() -> Counts {
{ lines: 0, words: 0, chars: 0, bytes: 0, max_line_length: 0, }
}
///|
fn Counts::add(self : Counts, other : Counts) -> Unit {
self.lines += other.lines
self.words += other.words
self.chars += other.chars
self.bytes += other.bytes
self.max_line_length = Int64::max(self.max_line_length, other.max_line_length)
}
///|
fn is_space_byte(b : Byte) -> Bool {
b is (b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C)
}
///|
async fn count_reader(
reader : &@io.Reader,
selection : Selection,
counts? : Counts,
) -> Counts {
let counts = match counts {
Some(counts) => counts
None => Counts::new()
}
let buffer = FixedArray::make(@stream.chunk_size, b'\x00')
let data = buffer.unsafe_reinterpret_as_bytes()
let mut in_word = false
let mut line_width : Int64 = 0
defer {
counts.max_line_length = Int64::max(counts.max_line_length, line_width)
}
while reader.read(buffer, max_len=buffer.length()) is count && count > 0 {
let chunk = data[:count]
if selection.bytes {
counts.bytes += count.to_int64()
}
if selection.chars {
counts.chars += count.to_int64()
}
if selection.lines || selection.words || selection.max_line_length {
for b in chunk {
if selection.lines && b is b'\n' {
counts.lines += 1
}
if selection.max_line_length {
if b is b'\n' {
counts.max_line_length = Int64::max(
counts.max_line_length,
line_width,
)
line_width = 0
} else if b is b'\t' {
line_width += 8 - line_width % 8
} else if b.to_int() >= 0x20 && b.to_int() <= 0x7E {
line_width += 1
}
}
if selection.words {
if is_space_byte(b) {
in_word = false
} else if !in_word {
in_word = true
counts.words += 1
}
}
}
}
}
counts
}
///|
priv struct Selection {
lines : Bool
words : Bool
chars : Bool
bytes : Bool
max_line_length : Bool
}
///|
fn pad_left(text : String, width : Int32) -> String {
let output = StringBuilder()
for _ in 0..<(width - text.length()) {
output.write_char(' ')
}
output.write_string(text)
output.to_string()
}
///|
fn render_counts(
counts : Counts,
selection : Selection,
name : String?,
width : Int32,
) -> String {
let parts : Array[String] = []
if selection.lines {
parts.push(pad_left(counts.lines.to_string(), width))
}
if selection.words {
parts.push(pad_left(counts.words.to_string(), width))
}
if selection.chars {
parts.push(pad_left(counts.chars.to_string(), width))
}
if selection.bytes {
parts.push(pad_left(counts.bytes.to_string(), width))
}
if selection.max_line_length {
parts.push(pad_left(counts.max_line_length.to_string(), width))
}
if name is Some(n) {
parts.push(n)
}
parts.join(" ")
}
///|
async fn count_width(files : Array[String], selection : Selection) -> Int32 {
let counters = [
selection.lines,
selection.words,
selection.chars,
selection.bytes,
selection.max_line_length,
]
.filter(value => value)
.length()
if files.is_empty() || (files.length() == 1 && counters == 1) {
return 1
}
let mut minimum = 1
let mut size : UInt64 = 0
for index, path in files {
if path == "-" {
minimum = 7
continue
}
let bytes = try {
if @async_fs.kind(path) == Regular {
Some(@async_fs.file_size(path))
} else {
None
}
} catch {
_ => {
if index == 0 {
return 1
}
continue
}
}
match bytes {
Some(bytes) => {
let next = size + bytes.reinterpret_as_uint64()
size = if next < size { 0xFFFFFFFFFFFFFFFFUL } else { next }
}
None => minimum = 7
}
}
let mut width = 1
while size >= 10 {
size /= 10
width += 1
}
width.max(minimum)
}
///|
priv enum TotalMode {
Auto
Always
Only
Never
}
///|
fn parse_total_mode(value : String) -> TotalMode raise CliError {
let choices = ["auto", "always", "only", "never"]
let matches = choices.filter(choice => choice.has_prefix(value))
guard matches is [choice] else {
let kind = if matches.is_empty() { "invalid" } else { "ambiguous" }
raise CliError(
"wc: \{kind} argument '\{value}' for '--total'\nValid arguments are:\n - 'auto'\n - 'always'\n - 'only'\n - 'never'\nTry 'wc --help' for more information.",
)
}
match choice {
"auto" => Auto
"always" => Always
"only" => Only
_ => Never
}
}
///|
fn nul_file_names(data : Bytes) -> Array[String] raise {
let names : Array[String] = []
let mut start = 0
for index, byte in data {
if byte is b'\x00' {
names.push(@utf8.decode(data[start:index].to_owned()))
start = index + 1
}
}
if start < data.length() {
names.push(@utf8.decode(data[start:].to_owned()))
}
names
}
///|
async fn read_file_list(path : String) -> Array[String] {
let data = if path == "-" {
@stdio.stdin.read_all().binary()
} else {
@async_fs.read_file(path).binary()
}
nul_file_names(data)
}
///|
fn quoted_name(path : String) -> String {
if !path.is_empty() &&
path
.iter()
.all(char => {
char is ('a'..<='z' | 'A'..<='Z' | '0'..<='9') ||
"_./=-+,:".contains(char.to_string())
}) {
path
} else {
"'" + path.replace_all(old="'", new="'\\''") + "'"
}
}
///|
fn input_error(path : String, error : Error) -> String {
let message = match error {
@os_error.OSError(_, ..) as os_error =>
if os_error.is_ENOENT() {
"No such file or directory"
} else if os_error.is_EACCES() {
"Permission denied"
} else if os_error.is_ENOTDIR() {
"Not a directory"
} else {
os_error.to_string()
}
_ => error.to_string()
}
"wc: " + quoted_name(path) + ": " + message
}
///|
async fn read_counted(
reader : &@io.Reader,
path : String,
selection : Selection,
) -> (Counts?, String?) {
let counts = Counts::new()
let error = try {
ignore(count_reader(reader, selection, counts~))
None
} catch {
error => Some(input_error(path, error))
}
(Some(counts), error)
}
///|
async fn count_source(
path : String,
selection : Selection,
) -> (Counts?, String?) {
if path == "-" {
read_counted(@stdio.stdin, path, selection)
} else {
let file = @async_fs.open(path, mode=ReadOnly) catch {
error => return (None, Some(input_error(path, error)))
}
defer file.close()
if file.kind() == Directory {
(
Some(Counts::new()),
Some("wc: " + quoted_name(path) + ": Is a directory"),
)
} else {
read_counted(file, path, selection)
}
}
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("wc 0.2.2\n")
return
}
let args = @env.args()[1:]
let parsed = @cli.parse(args, [
@cli.flag("lines", short='l'),
@cli.flag("words", short='w'),
@cli.flag("bytes", short='c'),
@cli.flag("chars", short='m'),
@cli.flag("max-line-length", short='L'),
@cli.option("files0-from"),
@cli.option("total"),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
if option == "total" {
@stdio.stderr.write(
"wc: option '--total' requires an argument\nTry 'wc --help' for more information.\n",
)
@sys.exit(1)
return
}
@stdio.stderr.write("wc: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(
"Usage: wc [-lwcmL] [--files0-from=FILE] [--total=WHEN] [FILE...]\n --total=WHEN print totals: auto (default), always, only, never\n --help display this help and exit\n --version display version and exit\nCounting uses the fixed C byte profile; -m counts every byte.\n",
)
return
}
let mode = parse_total_mode(parsed.last_value("total").unwrap_or("auto")) catch {
CliError(message) => {
@stdio.stderr.write(message + "\n")
@sys.exit(1)
return
}
}
let lines = parsed.contains("lines")
let words = parsed.contains("words")
let chars = parsed.contains("chars")
let bytes = parsed.contains("bytes")
let max_line_length = parsed.contains("max-line-length")
let selection : Selection = if lines ||
words ||
chars ||
bytes ||
max_line_length {
{ lines, words, chars, bytes, max_line_length, }
} else {
{
lines: true,
words: true,
chars: false,
bytes: true,
max_line_length: false,
}
}
if parsed.contains("files0-from") && !parsed.operands.is_empty() {
@stdio.stderr.write("wc: extra operand with --files0-from\n")
@sys.exit(1)
return
}
let files = match parsed.last_value("files0-from") {
Some(path) =>
read_file_list(path) catch {
err => {
@stdio.stderr.write("wc: \{err}\n")
@sys.exit(1)
return
}
}
None => parsed.operands
}
let implicit_stdin = files.is_empty() && !parsed.contains("files0-from")
let sources = if implicit_stdin { ["-"] } else { files }
let width = if mode is Only { 1 } else { count_width(sources, selection) }
let total = Counts::new()
let mut failed = false
for index, path in sources {
if path == "" {
let prefix = match parsed.last_value("files0-from") {
Some(list) => quoted_name(list) + ":" + (index + 1).to_string() + ": "
None => ""
}
@stdio.stderr.write("wc: " + prefix + "invalid zero-length file name\n")
failed = true
continue
}
if path == "-" && parsed.last_value("files0-from") == Some("-") {
@stdio.stderr.write(
"wc: when reading file names from standard input, no file name of '-' allowed\n",
)
failed = true
continue
}
let (counts, error) = count_source(path, selection)
if error is Some(message) {
@stdio.stderr.write(message + "\n")
failed = true
}
if counts is Some(counts) {
total.add(counts)
if !(mode is Only) {
let name = if implicit_stdin { None } else { Some(path) }
@stdio.stdout.write(
render_counts(counts, selection, name, width) + "\n",
)
}
}
}
if mode is (Always | Only) || (mode is Auto && files.length() > 1) {
@stdio.stdout.write(
render_counts(
total,
selection,
if mode is Only {
None
} else {
Some("total")
},
width,
) +
"\n",
)
}
if failed {
@sys.exit(1)
}
}