///|
struct Output {
prefix : String
width : Int32
suffix : SuffixFormat?
quiet : Bool
elide : Bool
paths : Array[String]
}
///|
priv suberror OutputNameError {
UnsupportedOutputName
}
///|
fn Output::path(self : Output, number : Int32) -> String raise OutputNameError {
let suffix = match self.suffix {
Some(format) => format.render(number)
None =>
String::make(
@cmp.maximum(0, self.width - number.to_string().length()),
'0',
) +
number.to_string()
}
let path = self.prefix + suffix
if @platform.is_windows() && (path.has_suffix(" ") || path.has_suffix(".")) {
raise UnsupportedOutputName
}
path
}
///|
async fn Output::write(
self : Output,
input : Snapshot,
first : Int64,
end : Int64,
) -> Unit {
let start = input.offset(first)
let stop = input.offset(end)
if self.elide && start == stop {
return
}
let path = self.path(self.paths.length())
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()
input.copy(start, stop, file)
if !self.quiet {
@stdio.stdout.write("\{stop - start}\n")
}
}
///|
async fn partition(
data : Snapshot,
controls : Array[Control],
output : Output,
suppress : Bool,
) -> Unit {
let count = data.records
let mut first = 0L
let mut searched = -1L
for control in controls {
let mut repetition = 0L
while control.repeats is None ||
(control.repeats is Some(n) && repetition <= n.to_int64()) {
let (cut, ignored) = match control.pattern {
Line(number) => {
let target = number.to_int64() * (repetition + 1L) - 1L
if target >= count {
output.write(data, first, count)
fail("line number out of range")
}
let target = @cmp.maximum(first, target)
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 = begin; line < count; line = line + 1 {
if data.matches(line, regex) {
found = Some(line)
break
}
}
match found {
None => {
if !ignored {
output.write(data, first, count)
}
if control.repeats is None {
return
}
fail("match not found")
}
Some(line) => {
let target = line + offset.to_int64()
if target < first || target > count {
if !ignored && target > count {
output.write(data, first, count)
}
fail("line number out of range")
}
searched = @cmp.maximum(line, target)
(target, ignored)
}
}
}
}
if !ignored {
output.write(data, first, cut)
}
first = if suppress && cut < count { cut + 1 } else { cut }
repetition += 1
}
}
output.write(data, first, count)
}
///|
async fn process(
reader : &@io.Reader,
controls : Array[Control],
output : Output,
suppress : Bool,
) -> Unit {
process_spool(
@fs.tmpdir(prefix="csplit-"),
reader,
controls,
output,
suppress,
)
}
///|
async fn process_spool(
directory : String,
reader : &@io.Reader,
controls : Array[Control],
output : Output,
suppress : Bool,
) -> Unit {
defer @fs.rmdir(directory, recursive=true)
let data = @fs.open(
directory + "/input",
mode=ReadWrite,
create_mode=CreateNew,
permission=0o600,
)
defer data.close()
let index = @fs.open(
directory + "/index",
mode=ReadWrite,
create_mode=CreateNew,
permission=0o600,
)
defer index.close()
partition(snapshot(reader, data, index), controls, output, suppress)
}
///|
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).
#| -b, --suffix-format=FORMAT Use one d/i/u/o/x/X conversion for suffixes.
#| --suppress-matched Omit the offset-adjusted split boundary line.
#| -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
#|
#|BRE matching includes backreferences, byte classes and word assertions.
#|Existing output paths and locale-specific matching are unsupported.
#|Windows output names ending in spaces or dots are rejected before input.
#|Input is snapshotted with a disk index; temporary write access is required.
message
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("csplit 0.2.1\n")
return
}
let parsed = @cli.parse(@env.args()[1:], [
@cli.option("prefix", short='f'),
@cli.option("digits", short='n'),
@cli.option("suffix-format", short='b'),
@cli.flag("suppress-matched"),
@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.2.1\n")
return
}
let (controls, width, suffix) = 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")
}
let suffix = parsed.last_value("suffix-format").map(suffix_format)
(parse_controls(parsed.operands[1:]), width, suffix)
} catch {
SuffixError(message) => {
@stdio.stderr.write("csplit: " + message + "\n")
@sys.exit(1)
return
}
@bytere.PatternError(message) => {
@stdio.stderr.write("csplit: \{message}\n")
@sys.exit(2)
return
}
err => {
@stdio.stderr.write("csplit: \{err}\n")
@sys.exit(2)
return
}
}
let output : Output = {
prefix: parsed.last_value("prefix").unwrap_or("xx"),
width,
suffix,
quiet: parsed.contains("silent") || parsed.contains("quiet"),
elide: parsed.contains("elide-empty-files"),
paths: [],
}
try {
ignore(output.path(0))
let path = parsed.operands[0]
let source = if path == "-" {
None
} else {
Some(@fs.open(path, mode=ReadOnly))
}
defer (if source is Some(file) { file.close() })
let reader : &@io.Reader = match source {
Some(file) => {
let reader : &@io.Reader = file
reader
}
None => {
let reader : &@io.Reader = @stdio.stdin
reader
}
}
process(reader, controls, output, parsed.contains("suppress-matched"))
} catch {
err => {
let name_boundary = err is UnsupportedOutputName
@stdio.stderr.write(
if name_boundary {
"csplit: unsupported capability: verbatim output names ending in spaces or dots\n"
} else {
"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(if name_boundary { 2 } else { 1 })
}
}
}