///|
priv suberror SplitError {
SplitError(Int32, String)
}
///|
priv enum Method {
BytesPer(Int64)
LinesPer(Int64)
LineBytes(Int64)
Chunks(Int64, Int64, Bool)
RoundRobin(Int64, Int64)
}
///|
priv struct Options {
layout : Method
prefix : String
additional : String
alphabet : String
start : String?
width : Int32
automatic : Bool
elide : Bool
filter : String?
}
///|
fn usage_error(message : String) -> Unit raise SplitError {
raise SplitError(1, message + "\nTry 'split --help' for more information.")
}
///|
fn positive(
value : String,
label~ : String = "invalid positive count",
) -> Int64 raise SplitError {
let mut start = 0
while start < value.length() &&
value[start] is (' ' | '\t' | '\n' | '\r' | '\u000b' | '\u000c') {
start += 1
}
if start < value.length() && value[start] == '+' {
start += 1
}
if start == value.length() {
raise SplitError(1, "\{label}: '\{value}'")
}
for index in start.. '9' {
raise SplitError(1, "\{label}: '\{value}'")
}
}
let count = @string.parse_int64(value[start:]) catch {
_ => 9223372036854775807L
}
if count <= 0 {
raise SplitError(1, "\{label}: '\{value}'")
}
count
}
///|
fn byte_count(
value : String,
label~ : String = "invalid number of bytes",
) -> Int64 raise SplitError {
let mut start = 0
while start < value.length() &&
value[start] is (' ' | '\t' | '\n' | '\r' | '\u000b' | '\u000c') {
start += 1
}
if start < value.length() && value[start] == '+' {
start += 1
}
let mut end = start
while end < value.length() && value[end] >= '0' && value[end] <= '9' {
end += 1
}
if end == start {
raise SplitError(1, "\{label}: '\{value}'")
}
let count = @string.parse_int64(value[start:end]) catch {
_ => 9223372036854775807L
}
if count == 0 {
raise SplitError(1, "\{label}: '\{value}'")
}
let multiplier = match value[end:] {
"" => 1L
"b" => 512L
"kB" => 1000L
"K" | "KiB" => 1024L
"MB" => 1000000L
"M" | "MiB" => 1048576L
"GB" => 1000000000L
"G" | "GiB" => 1073741824L
_ => raise SplitError(1, "\{label}: '\{value}'")
}
if count > 9223372036854775807L / multiplier {
9223372036854775807L
} else {
count * multiplier
}
}
///|
fn normalized_args(args : ArrayView[String]) -> Array[String] {
let result : Array[String] = []
let mut options = true
let mut value_next = false
for arg in args {
if value_next {
result.push(arg)
value_next = false
continue
}
if options && arg == "--" {
options = false
}
result.push(
if options && arg == "--numeric-suffixes" {
"-d"
} else if options && arg == "--hex-suffixes" {
"-x"
} else {
arg
},
)
if options {
if [
"--bytes", "--lines", "--line-bytes", "--number", "--suffix-length", "--additional-suffix",
"--filter",
].contains(arg) {
value_next = true
} else if arg.has_prefix("-") && !arg.has_prefix("--") {
for index in 1.. @cli.ParsedArgs raise @cli.CliError {
@cli.parse(normalized_args(args), [
@cli.option("lines", short='l'),
@cli.option("bytes", short='b'),
@cli.option("line-bytes", short='C'),
@cli.option("number", short='n'),
@cli.option("suffix-length", short='a'),
@cli.option("additional-suffix"),
@cli.option("numeric-suffixes"),
@cli.flag("numeric-suffixes", short='d'),
@cli.option("hex-suffixes"),
@cli.flag("hex-suffixes", short='x'),
@cli.flag("elide-empty-files", short='e'),
@cli.option("filter"),
@cli.flag("help"),
@cli.flag("version"),
])
}
///|
fn chunk_method(value : String) -> Method raise SplitError {
let mut start = 0
while start < value.length() &&
value[start] is (' ' | '\t' | '\n' | '\r' | '\u000b' | '\u000c') {
start += 1
}
let value = value[start:].to_owned()
let (text, lines, round_robin) = if value.has_prefix("l/") {
(value[2:].to_owned(), true, false)
} else if value.has_prefix("r/") {
(value[2:].to_owned(), false, true)
} else {
(value, false, false)
}
let parts = text.split("/").collect()
let (selected, count) = match parts[:] {
[count] =>
(0L, positive(count.to_owned(), label="invalid number of chunks"))
[selected, count] => {
let count = positive(count.to_owned(), label="invalid number of chunks")
let selected = positive(selected.to_owned(), label="invalid chunk number")
if selected > count {
raise SplitError(1, "invalid chunk number: '\{selected}'")
}
(selected, count)
}
_ => raise SplitError(1, "invalid number of chunks: '\{text}'")
}
if round_robin {
RoundRobin(count, selected)
} else {
Chunks(count, selected, lines)
}
}
///|
fn options(parsed : @cli.ParsedArgs) -> Options raise SplitError {
let mut chosen : Method? = None
let mut alphabet = "abcdefghijklmnopqrstuvwxyz"
let mut start : String? = None
let mut requested = 0
let mut additional = ""
let positions : Map[String, Int32] = Map([])
for name in parsed.order {
let index = positions.get(name).unwrap_or(0)
positions[name] = index + 1
let value = parsed.occurrences
.get(name)
.unwrap_or([])
.get(index)
.unwrap_or(None)
if ["bytes", "lines", "line-bytes", "number"].contains(name) {
if chosen is Some(_) {
usage_error("cannot split in more than one way")
}
chosen = Some(
match name {
"bytes" => BytesPer(byte_count(value.unwrap()))
"line-bytes" =>
LineBytes(
byte_count(value.unwrap(), label="invalid number of lines"),
)
"number" => chunk_method(value.unwrap())
_ =>
LinesPer(positive(value.unwrap(), label="invalid number of lines"))
},
)
} else if name == "suffix-length" {
requested = @string.parse_int(value.unwrap()) catch {
_ => raise SplitError(1, "invalid suffix length")
}
if requested < 0 {
raise SplitError(1, "invalid suffix length")
}
} else if name == "numeric-suffixes" || name == "hex-suffixes" {
alphabet = if name == "numeric-suffixes" {
"0123456789"
} else {
"0123456789abcdef"
}
if value is Some(value) {
for character in value {
if !alphabet.contains(character.to_string()) {
usage_error(
"'\{value}': invalid start value for \{if alphabet.length() == 16 { "hexadecimal" } else { "numerical" }} suffix",
)
}
}
let mut offset = 0
while offset + 1 < value.length() && value[offset] == '0' {
offset += 1
}
start = Some(value[offset:].to_owned())
}
} else if name == "additional-suffix" {
additional = value.unwrap()
let windows_separator = match @async.platform {
Windows => additional.contains("\\")
_ => false
}
if additional.contains("/") || windows_separator {
usage_error(
"invalid suffix '\{additional}', contains directory separator",
)
}
}
}
let layout = chosen.unwrap_or(LinesPer(1000L))
let selected = match layout {
Chunks(_, selected, _) | RoundRobin(_, selected) => selected
_ => 0L
}
if selected != 0 && parsed.contains("filter") {
usage_error(
"--filter does not process a chunk extracted to standard output",
)
}
if start is Some(value) {
for character in value {
if !alphabet.contains(character.to_string()) {
usage_error("suffix start contains a digit outside the final alphabet")
}
}
}
let count = match layout {
Chunks(count, _, _) | RoundRobin(count, _) => count
_ => 0L
}
let mut last = if count == 0 { 0L } else { count - 1 }
if count != 0 && start is Some(value) {
let from = @string.parse_int64(value) catch { _ => count }
if from >= 0 && from < count {
last = if last > 9223372036854775807L - from {
9223372036854775807L
} else {
last + from
}
}
}
let mut needed = 1
while last >= alphabet.length().to_int64() {
needed += 1
last /= alphabet.length().to_int64()
}
if requested != 0 && count != 0 && requested < needed {
raise SplitError(1, "the suffix length needs to be at least \{needed}")
}
let width = if requested != 0 {
requested
} else if needed > 2 {
needed
} else {
2
}
if parsed.operands.length() > 2 {
usage_error("extra operand '\{parsed.operands[2]}'")
}
if start is Some(value) && value.length() > width {
usage_error(
"numerical suffix start value is too large for the suffix length",
)
}
{
layout,
prefix: parsed.operands.get(1).unwrap_or("x"),
additional,
alphabet,
start,
width,
automatic: requested == 0 && start is None && count == 0,
elide: parsed.contains("elide-empty-files"),
filter: parsed.last_value("filter"),
}
}