///|
priv suberror CliError {
CliError(String)
}
///|
let open_end : Int32 = 0x7FFFFFFF
///|
fn parse_positive(text : String, spec : String) -> Int32 raise CliError {
let n = @string.parse_int(text) catch {
_ => raise CliError("cut: invalid list: '\{spec}'")
}
if n < 1 {
raise CliError("cut: list positions are numbered from 1: '\{spec}'")
}
n
}
///|
fn parse_list(spec : String) -> Array[(Int32, Int32)] raise CliError {
let ranges : Array[(Int32, Int32)] = []
for part in spec.split(",") {
let item = part.to_owned()
if item is "" {
raise CliError("cut: invalid list: '\{spec}'")
}
let pieces : Array[String] = item
.split("-")
.map(v => v.to_owned())
.collect()
match pieces {
[n] => {
let v = parse_positive(n, spec)
ranges.push((v, v))
}
["", ""] => raise CliError("cut: invalid list: '\{spec}'")
[a, b] => {
let lo = if a is "" { 1 } else { parse_positive(a, spec) }
let hi = if b is "" { open_end } else { parse_positive(b, spec) }
if lo > hi {
raise CliError("cut: invalid decreasing range: '\{spec}'")
}
ranges.push((lo, hi))
}
_ => raise CliError("cut: invalid list: '\{spec}'")
}
}
if ranges.is_empty() {
raise CliError("cut: invalid list: '\{spec}'")
}
normalize_ranges(ranges)
}
///|
fn normalize_ranges(ranges : Array[(Int32, Int32)]) -> Array[(Int32, Int32)] {
ranges.sort_by((a, b) => a.0.compare(b.0))
let merged : Array[(Int32, Int32)] = []
for range in ranges {
if !merged.is_empty() {
let last = merged.length() - 1
let (lo, hi) = merged[last]
if range.0 <= hi {
merged[last] = (lo, Int32::max(hi, range.1))
continue
}
}
// Adjacent explicit ranges retain their output-delimiter boundary.
merged.push(range)
}
merged
}
///|
fn complement_ranges(ranges : Array[(Int32, Int32)]) -> Array[(Int32, Int32)] {
let result : Array[(Int32, Int32)] = []
let mut start = 1
for (lo, hi) in ranges {
if start < lo {
result.push((start, lo - 1))
}
if hi == open_end {
return result
}
start = hi + 1
}
result.push((start, open_end))
result
}
///|
fn output_delimiter_bytes(value : String) -> Bytes {
if value == "" {
b"\x00"
} else {
@utf8.encode(value)
}
}
///|
fn cut_fields(
line : Bytes,
ranges : Array[(Int32, Int32)],
delimiter : Byte,
only_delimited : Bool,
output_delimiter : Bytes,
) -> Bytes? {
if !line.iter().any(byte => byte == delimiter) {
if only_delimited {
return None
}
return Some(line)
}
let kept = @buffer.Buffer(size_hint=line.length())
let mut start = 0
let mut field = 1
let mut range_index = 0
let mut first = true
for index in 0..<=line.length() {
if index == line.length() || line[index] == delimiter {
while range_index < ranges.length() && ranges[range_index].1 < field {
range_index += 1
}
if range_index < ranges.length() && ranges[range_index].0 <= field {
if !first {
kept.write_bytes(output_delimiter)
}
kept.write_bytes(line[start:index])
first = false
}
start = index + 1
field += 1
}
}
Some(kept.to_bytes())
}
///|
fn cut_characters(
line : Bytes,
ranges : Array[(Int32, Int32)],
output_delimiter : Bytes?,
) -> Bytes {
let out = @buffer.Buffer(size_hint=line.length())
let mut first = true
for (lo, hi) in ranges {
if lo > line.length() {
break
}
if !first && output_delimiter is Some(separator) {
out.write_bytes(separator)
}
out.write_bytes(line[lo - 1:Int32::min(hi, line.length())])
first = false
}
out.to_bytes()
}
///|
async fn cut_same_delimiter(
scanner : @stream.LineScanner,
ranges : Array[(Int32, Int32)],
delimiter : Byte,
only_delimited : Bool,
output_delimiter : Bytes,
writer~ : &@io.Writer = @stdio.stdout,
) -> Unit {
guard scanner.next() is Some(first) else { return }
let ending = Bytes::from_array([delimiter])
if !first.terminated {
if !only_delimited {
writer.write(first.data)
writer.write(ending)
}
return
}
let mut record = Some(first)
let mut field = 1
let mut range_index = 0
let mut selected_field = false
while record is Some(line) {
while range_index < ranges.length() && ranges[range_index].1 < field {
range_index += 1
}
if range_index < ranges.length() && ranges[range_index].0 <= field {
if selected_field {
writer.write(output_delimiter)
}
writer.write(line.data)
selected_field = true
}
field += 1
record = scanner.next()
}
if selected_field || !only_delimited || field > 2 {
writer.write(ending)
}
}
///|
async fn cut_no_fields(
reader : &@io.Reader,
delimiter : Byte,
record_delimiter : Byte,
only_delimited : Bool,
writer~ : &@io.Writer = @stdio.stdout,
) -> Unit {
// GNU 9.11's empty selection preserves its block-dependent undelimited fast path.
let buffer = FixedArray::make(256 * 1024, b'\x00')
let ending = Bytes::from_array([record_delimiter])
let mut start = 0
let mut limit = 0
let mut eof = false
for ;; {
let remaining = limit - start
if remaining < 4 && !eof {
for index in 0.. byte == delimiter) {
let mut stop = if eof { limit } else { start }
if !eof {
for index = limit - 1; index >= start; index = index - 1 {
if buffer[index] == record_delimiter {
stop = index + 1
break
}
}
}
if stop > start {
if !only_delimited {
writer.write(Bytes::from_array(buffer[start:stop]))
if eof && buffer[stop - 1] != record_delimiter {
writer.write(ending)
}
}
start = stop
continue
}
}
if !only_delimited {
for index in start.. Unit {
if field_ranges is Some(ranges) && ranges.is_empty() {
cut_no_fields(reader, delimiter, record_delimiter, only_delimited)
return
}
let scanner = @stream.LineScanner::new(reader, delimiter=record_delimiter)
let field_output_delimiter = output_delimiter.unwrap_or(
Bytes::from_array([delimiter]),
)
if field_ranges is Some(ranges) && delimiter == record_delimiter {
cut_same_delimiter(
scanner, ranges, delimiter, only_delimited, field_output_delimiter,
)
return
}
while scanner.next() is Some(line) {
match field_ranges {
Some(ranges) =>
match
cut_fields(
line.data,
ranges,
delimiter,
only_delimited,
field_output_delimiter,
) {
Some(out) => {
@stdio.stdout.write(out)
@stdio.stdout.write(Bytes::from_array([record_delimiter]))
}
None => ()
}
None =>
match char_ranges {
Some(ranges) => {
@stdio.stdout.write(
cut_characters(line.data, ranges, output_delimiter),
)
@stdio.stdout.write(Bytes::from_array([record_delimiter]))
}
None => ()
}
}
}
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("cut 0.2.2\n")
return
}
let args = @env.args()[1:]
let parsed = @cli.parse(args, [
@cli.flag("only-delimited", short='s'),
@cli.option("delimiter", short='d'),
@cli.option("fields", short='f'),
@cli.option("bytes", short='b'),
@cli.option("characters", short='c'),
@cli.flag("complement"),
@cli.option("output-delimiter"),
@cli.flag("zero-terminated", short='z'),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("cut: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(
"Usage: cut (-b LIST | -c LIST | -f LIST [-d CHAR] [-s]) [--complement] [--output-delimiter=STRING] [-z] [FILE...]\n --help display this help and exit\n --version display version and exit\n",
)
return
}
let only_delimited = parsed.contains("only-delimited")
let record_delimiter = if parsed.contains("zero-terminated") {
b'\x00'
} else {
b'\n'
}
let delimiter = match parsed.last_value("delimiter") {
Some(d) => {
let encoded = @utf8.encode(d)
if encoded.length() > 1 {
@stdio.stderr.write(
"cut: the delimiter must be a single byte in C locale\n",
)
@sys.exit(2)
return
}
if encoded.is_empty() {
b'\x00'
} else {
encoded[0]
}
}
None => b'\t'
}
let fields_spec = parsed.last_value("fields")
let chars_spec = parsed.last_value("characters")
let bytes_spec = parsed.last_value("bytes")
let mode_count = (if fields_spec is Some(_) { 1 } else { 0 }) +
(if chars_spec is Some(_) { 1 } else { 0 }) +
(if bytes_spec is Some(_) { 1 } else { 0 })
if mode_count > 1 {
@stdio.stderr.write(
"cut: only one list may be specified\nTry 'cut --help' for more information.\n",
)
@sys.exit(1)
return
} else if mode_count == 0 {
@stdio.stderr.write(
"cut: you must specify a list of fields (-f) or characters (-c)\n",
)
@sys.exit(2)
return
}
if fields_spec is None &&
(parsed.contains("delimiter") || parsed.contains("only-delimited")) {
@stdio.stderr.write(
if parsed.contains("delimiter") {
"cut: an input delimiter makes sense\n\tonly when operating on fields\nTry 'cut --help' for more information.\n"
} else {
"cut: suppressing non-delimited lines makes sense\n\tonly when operating on fields\nTry 'cut --help' for more information.\n"
},
)
@sys.exit(1)
return
}
let (field_ranges, char_ranges) = try {
let field_ranges = match fields_spec {
Some(spec) => Some(parse_list(spec))
None => None
}
let selected_chars = match chars_spec {
Some(spec) => Some(spec)
None => bytes_spec
}
let char_ranges = match selected_chars {
Some(spec) => Some(parse_list(spec))
None => None
}
if parsed.contains("complement") {
(field_ranges.map(complement_ranges), char_ranges.map(complement_ranges))
} else {
(field_ranges, char_ranges)
}
} catch {
CliError(msg) => {
@stdio.stderr.write("\{msg}\n")
@sys.exit(2)
return
}
}
let output_delimiter = parsed
.last_value("output-delimiter")
.map(output_delimiter_bytes)
let files = parsed.operands
let sources = if files.is_empty() { ["-"] } else { files }
let mut failed = false
for path in sources {
try {
if path == "-" {
process_reader(
@stdio.stdin, field_ranges, char_ranges, delimiter, record_delimiter, only_delimited,
output_delimiter,
)
} else {
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
process_reader(
file, field_ranges, char_ranges, delimiter, record_delimiter, only_delimited,
output_delimiter,
)
}
} catch {
err => {
@stdio.stderr.write("cut: \{err}\n")
failed = true
continue
}
}
}
if failed {
@sys.exit(1)
}
}