///|
priv suberror CliError {
CliError(String)
}
///|
fn pad_left(text : String, width : Int) -> String {
let sb = StringBuilder()
for _ in 0..<(width - text.length()) {
sb.write_char(' ')
}
sb.write_string(text)
sb.to_string()
}
///|
fn parse_count(text : String, option : String) -> Int raise CliError {
let count = @string.parse_int(text) catch {
_ => raise CliError("uniq: invalid number of \{option}: '\{text}'")
}
if count < 0 {
raise CliError("uniq: invalid number of \{option}: '\{text}'")
}
count
}
///|
fn is_blank(byte : Byte) -> Bool {
byte is (b' ' | b'\t')
}
///|
fn comparison_key(
line : Bytes,
skip_fields : Int,
skip_chars : Int,
check_chars : Int?,
ignore_case : Bool,
) -> Bytes {
let mut start = 0
for _ in 0.. Int::min(line.length(), start + width)
None => line.length()
}
if !ignore_case {
return line[start:end].to_owned()
}
let folded : Array[Byte] = []
for byte in line[start:end] {
let value = byte.to_int()
folded.push(
if value >= 0x41 && value <= 0x5A {
(value + 0x20).to_byte()
} else {
byte
},
)
}
Bytes::from_array(folded)
}
///|
async fn write_group(
line : Bytes,
count : Int,
show_count : Bool,
only_repeated : Bool,
only_unique : Bool,
delimiter : Byte,
) -> Unit {
let selected = if only_repeated && only_unique {
false
} else if only_repeated {
count > 1
} else if only_unique {
count == 1
} else {
true
}
if selected {
if show_count {
@stdio.stdout.write(pad_left(count.to_string(), 7) + " ")
}
@stdio.stdout.write(line)
@stdio.stdout.write(if delimiter is b'\x00' { b"\x00" } else { b"\n" })
}
}
///|
async fn process_reader(
reader : &@io.Reader,
show_count : Bool,
only_repeated : Bool,
only_unique : Bool,
ignore_case : Bool,
skip_fields : Int,
skip_chars : Int,
check_chars : Int?,
delimiter : Byte,
) -> Unit {
let scanner = @stream.LineScanner::new(reader, delimiter~)
let mut previous : Bytes? = None
let mut previous_key : Bytes? = None
let mut count = 0
while scanner.next() is Some(record) {
let key = comparison_key(
record.data,
skip_fields,
skip_chars,
check_chars,
ignore_case,
)
match (previous, previous_key) {
(Some(_), Some(old_key)) if old_key == key => count += 1
(Some(old), _) => {
write_group(
old, count, show_count, only_repeated, only_unique, delimiter,
)
previous = Some(record.data)
previous_key = Some(key)
count = 1
}
_ => {
previous = Some(record.data)
previous_key = Some(key)
count = 1
}
}
}
if previous is Some(value) {
write_group(value, count, show_count, only_repeated, only_unique, delimiter)
}
}
///|
async fn main {
let args = @env.args()[1:]
let parsed = @cli.parse(args, [
@cli.flag("count", short='c'),
@cli.flag("repeated", short='d'),
@cli.flag("unique", short='u'),
@cli.flag("ignore-case", short='i'),
@cli.option("skip-fields", short='f'),
@cli.option("skip-chars", short='s'),
@cli.option("check-chars", short='w'),
@cli.flag("zero-terminated", short='z'),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("uniq: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(
"Usage: uniq [-cduiz] [-f FIELDS] [-s CHARS] [-w CHARS] [FILE]\n",
)
return
}
if parsed.operands.length() > 1 {
@stdio.stderr.write("uniq: extra operand: '\{parsed.operands[1]}'\n")
@sys.exit(2)
return
}
let (skip_fields, skip_chars, check_chars) = try {
let fields = match parsed.last_value("skip-fields") {
Some(text) => parse_count(text, "fields")
None => 0
}
let chars = match parsed.last_value("skip-chars") {
Some(text) => parse_count(text, "characters")
None => 0
}
let width = match parsed.last_value("check-chars") {
Some(text) => Some(parse_count(text, "characters"))
None => None
}
(fields, chars, width)
} catch {
CliError(message) => {
@stdio.stderr.write(message + "\n")
@sys.exit(2)
return
}
}
let delimiter : Byte = if parsed.contains("zero-terminated") {
b'\x00'
} else {
b'\n'
}
let path = if parsed.operands.is_empty() { "-" } else { parsed.operands[0] }
try {
if path == "-" {
process_reader(
@stdio.stdin,
parsed.contains("count"),
parsed.contains("repeated"),
parsed.contains("unique"),
parsed.contains("ignore-case"),
skip_fields,
skip_chars,
check_chars,
delimiter,
)
} else {
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
process_reader(
file,
parsed.contains("count"),
parsed.contains("repeated"),
parsed.contains("unique"),
parsed.contains("ignore-case"),
skip_fields,
skip_chars,
check_chars,
delimiter,
)
}
} catch {
err => {
@stdio.stderr.write("uniq: \{err}\n")
@sys.exit(1)
return
}
}
}