///|
fn help_message() -> String {
let message =
#|Usage: cat [OPTION]... [FILE]...
#|
#|Concatenate FILEs (or stdin) to standard output, byte-transparently:
#|no decoding, no newline normalization. Use '-' or no file for stdin.
#|
#| -n, --number Number all output lines.
#| -b, --number-nonblank Number nonempty output lines (overrides -n).
#| -s, --squeeze-blank Suppress repeated empty output lines.
#| -T, --show-tabs Display TAB as ^I.
#| -E, --show-ends Display '$' at the end of each line.
#| -v, --show-nonprinting Display nonprinting bytes visibly.
#| -A, --show-all Equivalent to -vET.
#| -e Equivalent to -vE.
#| -t Equivalent to -vT.
#| -u, --unbuffered Accepted for GNU compatibility.
#| -h, --help Show this help message.
message
}
///|
fn append_char(output : Array[Byte], char : Char) -> Unit {
output.push(char.to_int().to_byte())
}
///|
fn append_text(output : Array[Byte], text : String) -> Unit {
for byte in @utf8.encode(text) {
output.push(byte)
}
}
///|
fn append_number(output : Array[Byte], number : Int64) -> Unit {
let text = number.to_string()
let padding = 6 - text.length()
for _ in 0.. Unit {
let value = byte.to_int()
if value == 0x09 && !show_tabs {
output.push(byte)
} else if value == 0x09 {
append_text(output, "^I")
} else if value < 0x20 {
append_char(output, '^')
append_char(output, (value + 0x40).unsafe_to_char())
} else if value == 0x7f {
append_text(output, "^?")
} else if value >= 0x80 {
append_text(output, "M-")
append_visible_byte(output, (value - 0x80).to_byte(), show_tabs)
} else {
output.push(byte)
}
}
///|
fn render_line(
line : Array[Byte],
output : Array[Byte],
number : Int64,
number_lines : Bool,
number_nonblank : Bool,
show_ends : Bool,
show_nonprinting : Bool,
show_tabs : Bool,
terminated : Bool,
) -> Unit {
let nonblank = line.length() > 0
if number_lines && (!number_nonblank || nonblank) {
append_number(output, number)
}
for byte in line {
if show_nonprinting {
append_visible_byte(output, byte, show_tabs)
} else if show_tabs && byte == b'\t' {
append_text(output, "^I")
} else {
output.push(byte)
}
}
if show_ends && terminated {
append_char(output, '$')
}
if terminated {
append_char(output, '\n')
}
}
///|
priv struct CatRenderer {
number : Bool
number_nonblank : Bool
squeeze : Bool
show_ends : Bool
show_nonprinting : Bool
show_tabs : Bool
line : Array[Byte]
mut line_number : Int64
mut previous_blank : Bool
}
///|
fn CatRenderer::new(options : @cli.ParsedArgs) -> CatRenderer {
{
number: options.contains("number"),
number_nonblank: options.contains("number-nonblank"),
squeeze: options.contains("squeeze-blank"),
show_ends: options.contains("show-ends") ||
options.contains("show-all") ||
options.contains("show-ends-nonprinting"),
show_nonprinting: options.contains("show-nonprinting") ||
options.contains("show-all") ||
options.contains("show-ends-nonprinting") ||
options.contains("show-tabs-nonprinting"),
show_tabs: options.contains("show-tabs") ||
options.contains("show-all") ||
options.contains("show-tabs-nonprinting"),
line: [],
line_number: 1,
previous_blank: false,
}
}
///|
fn CatRenderer::flush(self : CatRenderer, terminated : Bool) -> Bytes {
let blank = self.line.is_empty()
if terminated && self.squeeze && blank && self.previous_blank {
return b""
}
let output : Array[Byte] = []
render_line(
self.line,
output,
self.line_number,
self.number || self.number_nonblank,
self.number_nonblank,
self.show_ends,
self.show_nonprinting,
self.show_tabs,
terminated,
)
if (self.number || self.number_nonblank) && (!self.number_nonblank || !blank) {
self.line_number += 1
}
self.line.clear()
if terminated {
self.previous_blank = blank
}
Bytes::from_array(output)
}
///|
async fn CatRenderer::copy(self : CatRenderer, reader : &@io.Reader) -> Unit {
while @stream.read_chunk(reader) is Some(chunk) {
let output : Array[Byte] = []
for byte in chunk {
if byte == b'\n' {
for rendered in self.flush(true) {
output.push(rendered)
}
} else {
self.line.push(byte)
}
}
if !output.is_empty() {
@stdio.stdout.write(Bytes::from_array(output))
}
}
}
///|
async fn main {
let args = @env.args()[1:]
let parsed = @cli.parse(args, [
@cli.flag("number", short='n'),
@cli.flag("number-nonblank", short='b'),
@cli.flag("squeeze-blank", short='s'),
@cli.flag("show-tabs", short='T'),
@cli.flag("show-ends", short='E'),
@cli.flag("show-nonprinting", short='v'),
@cli.flag("show-all", short='A'),
@cli.flag("show-ends-nonprinting", short='e'),
@cli.flag("show-tabs-nonprinting", short='t'),
@cli.flag("unbuffered", short='u'),
@cli.flag("help", short='h'),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("cat: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(help_message() + "\n")
return
}
let operands = parsed.operands
let sources = if operands.is_empty() { ["-"][:] } else { operands }
let formatted = parsed.contains("number") ||
parsed.contains("number-nonblank") ||
parsed.contains("squeeze-blank") ||
parsed.contains("show-tabs") ||
parsed.contains("show-ends") ||
parsed.contains("show-nonprinting") ||
parsed.contains("show-all") ||
parsed.contains("show-ends-nonprinting") ||
parsed.contains("show-tabs-nonprinting")
let mut failed = false
let renderer = CatRenderer::new(parsed)
for path in sources {
try {
if path == "-" {
if formatted {
renderer.copy(@stdio.stdin)
} else {
@stdio.stdout.write_reader(@stdio.stdin)
}
} else {
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
if formatted {
renderer.copy(file)
} else {
@stdio.stdout.write_reader(file)
}
}
} catch {
err => {
@stdio.stderr.write("cat: \{err}\n")
failed = true
}
}
}
if formatted && !renderer.line.is_empty() {
@stdio.stdout.write(renderer.flush(false))
}
if failed {
@sys.exit(1)
}
}