///|
fn unexpand_bytes(
data : Bytes,
all_blanks : Bool,
first_only : Bool,
stops : @tabstops.TabStops,
) -> Bytes raise @tabstops.TabError {
let output : Array[Byte] = []
let pending : Array[Byte] = []
let mut column = 0L
let mut convert = true
let mut previous_blank = true
let mut one_blank_before_stop = false
fn flush_pending() {
if pending.length() > 1 && one_blank_before_stop {
pending[0] = b'\t'
}
output.append(pending)
pending.clear()
one_blank_before_stop = false
}
for byte in data {
let mut emit_byte = true
if convert {
let blank = byte is (b' ' | b'\t')
if blank {
match @tabstops.next(column, stops) {
None => convert = false
Some(stop) => {
if byte == b'\t' {
column = stop
if !pending.is_empty() {
pending[0] = b'\t'
}
} else {
column += 1
if !(previous_blank && column == stop) {
if column == stop {
one_blank_before_stop = true
}
pending.push(byte)
previous_blank = true
continue
}
output.push(b'\t')
emit_byte = false
if !pending.is_empty() {
pending[0] = b'\t'
}
}
if one_blank_before_stop {
pending.truncate(1)
} else {
pending.clear()
}
}
}
} else if byte == b'\b' {
if column > 0 {
column -= 1
}
} else if byte != b'\x00' {
if column == 9223372036854775807L {
raise @tabstops.TabError("input line is too long")
}
column += 1
}
flush_pending()
previous_blank = blank
convert = convert && ((all_blanks && !first_only) || blank)
}
if emit_byte {
output.push(byte)
}
if byte == b'\n' {
column = 0
convert = true
previous_blank = true
}
}
flush_pending()
Bytes::from_array(output)
}
///|
fn help_message() -> String {
let message =
#|Usage: unexpand [OPTION]... [FILE]...
#| --help display this help and exit
#| --version display version and exit
#|
#|Convert spaces in each FILE to tabs, writing to standard output.
#|With no FILE, or when FILE is -, read standard input.
#|
#| -a, --all convert all blanks, not just leading blanks
#| --first-only convert only leading blank sequences
#| -t, --tabs=LIST use ascending stops; /N absolute, +N relative (implies --all)
message
}
///|
async fn read_input(path : String) -> Bytes {
if path == "-" {
@stdio.stdin.read_all().binary()
} else {
@fs.read_file(path).binary()
}
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("unexpand 0.1.3\n")
return
}
let parsed = @cli.parse(@env.args()[1:], [
@cli.flag("all", short='a'),
@cli.flag("first-only"),
@cli.option("tabs", short='t'),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("unexpand: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(help_message() + "\n")
return
}
let stops = @tabstops.parse(parsed.values("tabs")) catch {
@tabstops.TabError(message) => {
@stdio.stderr.write("unexpand: \{message}\n")
@sys.exit(1)
return
}
}
let all_blanks = parsed.contains("all") || parsed.contains("tabs")
let sources = if parsed.operands.is_empty() {
["-"][:]
} else {
parsed.operands
}
let mut failed = false
for path in sources {
let data = read_input(path) catch {
err => {
@stdio.stderr.write("unexpand: \{path}: \{err}\n")
failed = true
continue
}
}
let unexpanded = unexpand_bytes(
data,
all_blanks,
parsed.contains("first-only"),
stops,
) catch {
@tabstops.TabError(message) => {
@stdio.stderr.write("unexpand: \{message}\n")
@sys.exit(1)
return
}
}
@stdio.stdout.write(unexpanded)
}
if failed {
@sys.exit(1)
}
}