///|
fn pad_left(text : String, width : Int, fill? : Char = ' ') -> String {
let sb = StringBuilder()
for _ in 0..<(width - text.length()) {
sb.write_char(fill)
}
sb.write_string(text)
sb.to_string()
}
///|
fn format_number(number : Int, width : Int, format : String) -> String {
let text = number.to_string()
match format {
"ln" => text + " ".repeat((width - text.length()).max(0))
"rz" if text.has_prefix("-") =>
"-" + pad_left(text[1:].to_owned(), width - 1, fill='0')
"rz" => pad_left(text, width, fill='0')
_ => pad_left(text, width)
}
}
///|
fn spaces(width : Int) -> String {
let sb = StringBuilder()
for _ in 0.. Unit {
let scanner = @stream.LineScanner::new(reader)
let header_marker = @utf8.encode(section_delimiter.repeat(3))
let body_marker = @utf8.encode(section_delimiter.repeat(2))
let footer_marker = @utf8.encode(section_delimiter)
while scanner.next() is Some(line) {
let next_section = if section_delimiter != "" &&
line.data[:].equal(header_marker[:]) {
Some(0)
} else if section_delimiter != "" && line.data[:].equal(body_marker[:]) {
Some(1)
} else if section_delimiter != "" && line.data[:].equal(footer_marker[:]) {
Some(2)
} else {
None
}
if next_section is Some(value) {
section.val = value
blank_run.val = 0
if !no_renumber {
number.val = starting_number
}
@stdio.stdout.write("\n")
continue
}
let style = match section.val {
0 => header_style
2 => footer_style
_ => body_style
}
let numbered = if line.data.is_empty() {
blank_run.val += 1
if style == "a" && blank_run.val == join_blank_lines {
blank_run.val = 0
true
} else {
false
}
} else {
blank_run.val = 0
style == "a" || style == "t"
}
if numbered {
@stdio.stdout.write(
format_number(number.val, width, number_format) + separator,
)
@stdio.stdout.write(line.data)
@stdio.stdout.write("\n")
number.val += increment
} else {
@stdio.stdout.write(spaces(width))
@stdio.stdout.write(separator)
@stdio.stdout.write(line.data)
@stdio.stdout.write("\n")
}
}
}
///|
async fn main {
let args = @env.args()[1:]
let parsed = @cli.parse(args, [
@cli.option("body-numbering", short='b'),
@cli.option("header-numbering", short='h'),
@cli.option("footer-numbering", short='f'),
@cli.option("number-width", short='w'),
@cli.option("number-separator", short='s'),
@cli.option("starting-line-number", short='v'),
@cli.option("line-increment", short='i'),
@cli.option("join-blank-lines", short='l'),
@cli.option("number-format", short='n'),
@cli.option("section-delimiter", short='d'),
@cli.flag("no-renumber", short='p'),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("nl: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(
"Usage: nl [-b STYLE] [-h STYLE] [-f STYLE] [-v NUMBER] [-i NUMBER] [-l NUMBER] [-n FORMAT] [-w NUMBER] [-s STRING] [-d CC] [-p] [FILE...]\n",
)
return
}
let body_style = parsed.last_value("body-numbering").unwrap_or("t")
let header_style = parsed.last_value("header-numbering").unwrap_or("n")
let footer_style = parsed.last_value("footer-numbering").unwrap_or("n")
for
entry in [
("body", body_style),
("header", header_style),
("footer", footer_style),
] {
let (label, style) = entry
if !(style is ("a" | "t" | "n")) {
@stdio.stderr.write("nl: invalid \{label} numbering style: '\{style}'\n")
@sys.exit(2)
return
}
}
let width_text = parsed.last_value("number-width").unwrap_or("6")
let width = @string.parse_int(width_text) catch {
_ => {
@stdio.stderr.write(
"nl: invalid line number field width: '\{width_text}'\n",
)
@sys.exit(2)
return
}
}
if width < 1 {
@stdio.stderr.write(
"nl: invalid line number field width: '\{width_text}'\n",
)
@sys.exit(2)
return
}
let separator = parsed.last_value("number-separator").unwrap_or("\t")
let starting_text = parsed.last_value("starting-line-number").unwrap_or("1")
let starting_number = @string.parse_int(starting_text) catch {
_ => {
@stdio.stderr.write(
"nl: invalid starting line number: '\{starting_text}'\n",
)
@sys.exit(2)
return
}
}
let increment_text = parsed.last_value("line-increment").unwrap_or("1")
let increment = @string.parse_int(increment_text) catch {
_ => {
@stdio.stderr.write(
"nl: invalid line number increment: '\{increment_text}'\n",
)
@sys.exit(2)
return
}
}
let join_blank_text = parsed.last_value("join-blank-lines").unwrap_or("1")
let join_blank_lines = @string.parse_int(join_blank_text) catch {
_ => {
@stdio.stderr.write(
"nl: invalid blank-line group size: '\{join_blank_text}'\n",
)
@sys.exit(2)
return
}
}
if join_blank_lines < 1 {
@stdio.stderr.write(
"nl: invalid blank-line group size: '\{join_blank_text}'\n",
)
@sys.exit(2)
return
}
let number_format = parsed.last_value("number-format").unwrap_or("rn")
if !(number_format is ("ln" | "rn" | "rz")) {
@stdio.stderr.write("nl: invalid line number format: '\{number_format}'\n")
@sys.exit(2)
return
}
let section_delimiter = match parsed.last_value("section-delimiter") {
Some(value) if @utf8.encode(value).length() == 1 => value + ":"
Some(value) => value
None => "\\:"
}
let files = parsed.operands
let sources = if files.is_empty() { ["-"] } else { files }
let number = Ref(starting_number)
let section = Ref(1)
let blank_run = Ref(0)
let no_renumber = parsed.contains("no-renumber")
let mut failed = false
for path in sources {
try {
if path == "-" {
process_reader(
@stdio.stdin, header_style, body_style, footer_style, width, separator,
number, starting_number, increment, number_format, join_blank_lines, no_renumber,
section_delimiter, section, blank_run,
)
} else {
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
process_reader(
file, header_style, body_style, footer_style, width, separator, number,
starting_number, increment, number_format, join_blank_lines, no_renumber,
section_delimiter, section, blank_run,
)
}
} catch {
err => {
@stdio.stderr.write("nl: \{err}\n")
failed = true
continue
}
}
}
if failed {
@sys.exit(1)
}
}