///|
priv suberror DelimiterError {
DelimiterError(String)
}
///|
fn parse_delimiters(spec : String) -> Array[Bytes] raise DelimiterError {
let delims : Array[Bytes] = []
let raw = @utf8.encode(spec)
let mut i = 0
while i < raw.length() {
if raw[i] == b'\\' && i + 1 < raw.length() {
let out = match raw[i + 1] {
b'b' => b"\b"
b'f' => b"\f"
b'n' => b"\n"
b'r' => b"\r"
b't' => b"\t"
b'v' => b"\x0b"
b'0' => b""
b'\\' => b"\\"
byte => Bytes::from_array([byte])
}
delims.push(out)
i += 2
} else if raw[i] == b'\\' {
raise DelimiterError(
"paste: delimiter list ends with an unescaped backslash",
)
} else {
delims.push(Bytes::from_array([raw[i]]))
i += 1
}
}
if delims.is_empty() {
delims.push(b"")
}
delims
}
///|
async fn paste_serial(
scanner : @stream.LineScanner,
delimiters : Array[Bytes],
record_delimiter : Byte,
) -> Unit {
let mut index = 0
while scanner.next() is Some(line) {
if index > 0 {
@stdio.stdout.write(delimiters[(index - 1) % delimiters.length()])
}
@stdio.stdout.write(line.data)
index += 1
}
@stdio.stdout.write(Bytes::from_array([record_delimiter]))
}
///|
async fn paste_parallel(
scanners : Array[@stream.LineScanner],
delimiters : Array[Bytes],
record_delimiter : Byte,
) -> Unit {
for ;; {
let row : Array[Bytes?] = []
let mut any = false
for scanner in scanners {
match scanner.next() {
Some(line) => {
row.push(Some(line.data))
any = true
}
None => row.push(None)
}
}
if !any {
break
}
for index, value in row {
if index > 0 {
@stdio.stdout.write(delimiters[(index - 1) % delimiters.length()])
}
if value is Some(data) {
@stdio.stdout.write(data)
}
}
@stdio.stdout.write(Bytes::from_array([record_delimiter]))
}
}
///|
async fn main {
let args = @env.args()[1:]
let parsed = @cli.parse(args, [
@cli.flag("serial", short='s'),
@cli.option("delimiters", short='d'),
@cli.flag("zero-terminated", short='z'),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("paste: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write("Usage: paste [-s] [-d LIST] [-z] [FILE...]\n")
return
}
let serial = parsed.contains("serial")
let record_delimiter = if parsed.contains("zero-terminated") {
b'\x00'
} else {
b'\n'
}
let delims = match parsed.last_value("delimiters") {
Some(spec) =>
parse_delimiters(spec) catch {
DelimiterError(message) => {
@stdio.stderr.write(message + "\n")
@sys.exit(1)
return
}
}
None => [b"\t"]
}
let files = parsed.operands
let sources = if files.is_empty() { ["-"] } else { files }
let open_files : Array[@fs.File] = []
defer (for file in open_files {
file.close()
})
let stdin_scanner = @stream.LineScanner::new(
@stdio.stdin,
delimiter=record_delimiter,
)
try {
if serial {
for path in sources {
if path == "-" {
paste_serial(stdin_scanner, delims, record_delimiter)
} else {
let file = @fs.open(path, mode=ReadOnly)
open_files.push(file)
paste_serial(
@stream.LineScanner::new(file, delimiter=record_delimiter),
delims,
record_delimiter,
)
}
}
} else {
let scanners : Array[@stream.LineScanner] = []
for path in sources {
if path == "-" {
scanners.push(stdin_scanner)
} else {
let file = @fs.open(path, mode=ReadOnly)
open_files.push(file)
scanners.push(
@stream.LineScanner::new(file, delimiter=record_delimiter),
)
}
}
paste_parallel(scanners, delims, record_delimiter)
}
} catch {
err => {
@stdio.stderr.write("paste: \{err}\n")
@sys.exit(1)
return
}
}
}