///|
fn starts_with_at(data : Bytes, index : Int, separator : Bytes) -> Bool {
if index + separator.length() > data.length() {
return false
}
for offset in 0.. (Array[Bytes], Bool) {
let records : Array[Bytes] = []
let mut start = 0
let mut index = 0
while index + separator.length() <= data.length() {
if starts_with_at(data, index, separator) {
records.push(data[start:index].to_owned())
start = index + separator.length()
index = start
} else {
index += 1
}
}
let trailing = data.length() >= separator.length() &&
starts_with_at(data, data.length() - separator.length(), separator)
if start < data.length() {
records.push(data[start:].to_owned())
}
(records, trailing)
}
///|
fn reverse_records(data : Bytes, separator : Bytes, before : Bool) -> Bytes {
if data.is_empty() {
return b""
}
let (records, trailing) = split_records(data, separator)
if records.is_empty() {
return data
}
let output : Array[Byte] = []
let mut index = records.length() - 1
while index >= 0 {
if before && (trailing || index != records.length() - 1) {
output.append(separator.to_array())
}
output.append(records[index].to_array())
if !before && index != 0 {
output.append(separator.to_array())
}
index -= 1
}
if !before && trailing {
output.append(separator.to_array())
}
Bytes::from_array(output)
}
///|
fn help_message() -> String {
let message =
#|Usage: tac [OPTION]... [FILE]...
#|
#|Write each FILE to standard output, last line first.
#|With no FILE, or when FILE is -, read standard input.
#|
#| -b, --before Attach the separator before records.
#| -s, --separator=STRING Use STRING instead of newline.
#| -r, --regex Not supported: regular separators need a public
#| regular-expression API with byte semantics.
#| --help Show this help message.
message
}
///|
async fn main {
let parsed = @cli.parse(@env.args()[1:], [
@cli.flag("before", short='b'),
@cli.option("separator", short='s'),
@cli.flag("regex", short='r'),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("tac: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(help_message() + "\n")
return
}
if parsed.contains("regex") {
@stdio.stderr.write(
"tac: regular expression separators are not supported by this build\n",
)
@sys.exit(2)
return
}
if parsed.operands.length() > 1 {
@stdio.stderr.write("tac: multiple input files are not supported\n")
@sys.exit(2)
return
}
let separator = @utf8.encode(parsed.last_value("separator").unwrap_or("\n"))
if separator.is_empty() {
@stdio.stderr.write("tac: separator must not be empty\n")
@sys.exit(2)
return
}
let path = if parsed.operands.is_empty() { "-" } else { parsed.operands[0] }
let data = try {
if path == "-" {
@stdio.stdin.read_all().binary()
} else {
@fs.read_file(path).binary()
}
} catch {
err => {
@stdio.stderr.write("tac: \{err}\n")
@sys.exit(1)
return
}
}
@stdio.stdout.write(
reverse_records(data, separator, parsed.contains("before")),
)
}