///|
struct Output {
prefix : String
width : Int
quiet : Bool
elide : Bool
paths : Array[String]
}
///|
async fn Output::write(self : Output, content : BytesView) -> Unit {
if self.elide && content.is_empty() {
return
}
let number = self.paths.length().to_string()
let suffix = String::make(@cmp.maximum(0, self.width - number.length()), '0') +
number
let path = self.prefix + suffix
if @fs.exists(path) {
fail("existing output is unsupported: \{path}")
}
let file = @fs.create(path, allow_existing=false)
self.paths.push(path)
defer file.close()
file.write(content)
if !self.quiet {
@stdio.stdout.write("\{content.length()}\n")
}
}
///|
async fn partition(
data : Bytes,
controls : Array[Control],
output : Output,
) -> Unit {
let starts = line_starts(data)
let count = starts.length() - 1
let mut first = 0
let mut searched = -1
for control in controls {
let mut repetition = 0
while control.repeats is None ||
(control.repeats is Some(n) && repetition <= n) {
let (cut, ignored) = match control.pattern {
Line(number) => {
let target = number.to_int64() * (repetition.to_int64() + 1L) - 1L
if target >= count.to_int64() {
output.write(data[starts[first]:])
fail("line number out of range")
}
let target = @cmp.maximum(first, target.to_int())
searched = @cmp.maximum(searched, target - 1)
(target, false)
}
Expression(regex, offset, ignored) => {
let mut found = None
let begin = @cmp.maximum(first, searched + 1)
for line in begin.. {
if !ignored {
output.write(data[starts[first]:])
}
if control.repeats is None {
return
}
fail("match not found")
}
Some(line) => {
let target = line.to_int64() + offset.to_int64()
if target < first.to_int64() || target > count.to_int64() {
if !ignored && target > count.to_int64() {
output.write(data[starts[first]:])
}
fail("line number out of range")
}
searched = @cmp.maximum(line, target.to_int())
(target.to_int(), ignored)
}
}
}
}
if !ignored {
output.write(data[starts[first]:starts[cut]])
}
first = cut
repetition += 1
}
}
output.write(data[starts[first]:])
}
///|
fn help_message() -> String {
let message =
#|Usage: csplit [OPTION]... FILE PATTERN...
#|Split FILE (- for stdin) at line numbers or basic regular expressions.
#|Patterns: NUMBER, /REGEXP/[OFFSET], %REGEXP%[OFFSET], {N}, {*}
#|
#| -f, --prefix=PREFIX Use PREFIX instead of xx.
#| -n, --digits=NUMBER Use at least NUMBER suffix digits (default 2).
#| -s, --silent Suppress output byte counts.
#| -q, --quiet Same as --silent.
#| -k, --keep-files Keep created fragments after a split error.
#| -z, --elide-empty-files Omit empty output fragments.
#| --help display this help and exit
#| --version display version and exit
#|
#|Existing output paths, custom suffix formats, backreferences, non-ASCII
#|regex patterns, and locale-specific matching are unsupported.
message
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("csplit 0.1.1\n")
return
}
let parsed = @cli.parse(@env.args()[1:], [
@cli.option("prefix", short='f'),
@cli.option("digits", short='n'),
@cli.flag("silent", short='s'),
@cli.flag("quiet", short='q'),
@cli.flag("keep-files", short='k'),
@cli.flag("elide-empty-files", short='z'),
@cli.flag("help"),
@cli.flag("version"),
]) catch {
@cli.CliError(message~, option~, ..) => {
@stdio.stderr.write("csplit: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(help_message() + "\n")
return
}
if parsed.contains("version") {
@stdio.stdout.write("csplit 0.1.1\n")
return
}
let (controls, width) = try {
guard parsed.operands.length() >= 2 else {
fail("FILE and PATTERN are required")
}
let width = @string.parse_int(parsed.last_value("digits").unwrap_or("2"))
guard width >= 0 && width <= 1024 else {
fail("suffix digit count must be between 0 and 1024")
}
(parse_controls(parsed.operands[1:]), width)
} catch {
err => {
@stdio.stderr.write("csplit: \{err}\n")
@sys.exit(2)
return
}
}
let output : Output = {
prefix: parsed.last_value("prefix").unwrap_or("xx"),
width,
quiet: parsed.contains("silent") || parsed.contains("quiet"),
elide: parsed.contains("elide-empty-files"),
paths: [],
}
try {
let path = parsed.operands[0]
let data = if path == "-" {
@stdio.stdin.read_all().binary()
} else {
@fs.read_file(path).binary()
}
partition(data, controls, output)
} catch {
err => {
@stdio.stderr.write("csplit: \{err}\n")
if !parsed.contains("keep-files") {
for path in output.paths {
@fs.remove(path) catch {
cleanup => @stdio.stderr.write("csplit: cleanup: \{cleanup}\n")
}
}
}
@sys.exit(1)
}
}
}