// Subcommand handlers and their shared helpers. Each handler reads its
// parsed arguments from `Matches`, performs the workbook operation, and
// prints a result.
///|
/// Returns the single value of a positional/option, or "" when absent.
fn arg(m : @argparse.Matches, name : String) -> String {
match m.values.get(name) {
Some(values) if values.length() > 0 => values[0]
_ => ""
}
}
///|
/// The sheet to operate on: the requested name, or the first sheet when
/// none was given.
fn resolve_sheet(workbook : @xl.Workbook, requested : String) -> String {
if requested != "" {
requested
} else {
workbook.get_sheet_name(0).unwrap_or("")
}
}
///|
/// Set a cell, storing plain numbers as real numeric cells (so formulas
/// evaluate and number formats render) and everything else as text.
fn set_cell_auto(
workbook : @xl.Workbook,
sheet : String,
cell : String,
raw : String,
) -> Unit raise @xl.XlsxError {
match classify_cell_value(raw) {
AsInt(value) => workbook.set_cell_int(sheet, cell, value)
AsFloat(value) => workbook.set_cell_float(sheet, cell, value)
AsText => workbook.set_cell(sheet, cell, raw)
}
}
///|
async fn run_create(m : @argparse.Matches) -> Unit {
let output = arg(m, "output")
let sheet = arg(m, "sheet")
let workbook = @lib.new_workbook()
ignore(workbook.add_sheet(sheet))
write_file(output, @lib.write(workbook))
println("created \{output} (sheet \{sheet})")
}
///|
async fn run_set(m : @argparse.Matches) -> Unit {
let file = arg(m, "file")
let sheet = arg(m, "sheet")
let cell = arg(m, "cell")
let value = arg(m, "value")
let workbook = @lib.open_file(file)
set_cell_auto(workbook, sheet, cell, value)
write_file(file, @lib.write(workbook))
println("set \{sheet}!\{cell} = \{value}")
}
///|
async fn run_get(m : @argparse.Matches) -> Unit {
if flag(m, "json") {
run_get_json(m)
return
}
let workbook = @lib.open_file(arg(m, "file"))
let sheet = arg(m, "sheet")
let cell = arg(m, "cell")
let value = workbook.get_cell(sheet, cell).unwrap_or("")
if value != "" {
println(value)
} else {
// A formula cell has no cached value (the tool doesn't evaluate formulas),
// so fall back to showing the formula itself rather than a blank line.
match workbook.get_cell_formula(sheet, cell) {
Some(formula) => println("=" + formula)
None => println("")
}
}
}
///|
async fn run_calc(m : @argparse.Matches) -> Unit {
let file = arg(m, "file")
let workbook = @lib.open_file(file)
// `--json` prints the typed computed value (xlsx.calc/1) — the gap
// `get --json` leaves for an uncached formula. Otherwise print the string:
// `--raw` is the unformatted result; the default applies the cell's number
// format, like Excel's displayed value.
if flag(m, "json") {
println(
@inspect.calc(workbook, file~, arg(m, "sheet"), arg(m, "cell")).stringify(
indent=2,
),
)
return
}
println(
workbook.calc_cell_value(
arg(m, "sheet"),
arg(m, "cell"),
raw=flag(m, "raw"),
),
)
}
///|
async fn run_sheets(m : @argparse.Matches) -> Unit {
let workbook = @lib.open_file(arg(m, "file"))
for name in workbook.get_sheet_list() {
println(name)
}
}
///|
async fn run_rows(m : @argparse.Matches) -> Unit {
let workbook = @lib.open_file(arg(m, "file"))
let sheet = resolve_sheet(workbook, arg(m, "sheet"))
for row in workbook.get_rows(sheet) {
println(row.map(csv_field).join(","))
}
}
///|
async fn run_import_csv(m : @argparse.Matches) -> Unit {
let input = arg(m, "input")
let output = arg(m, "output")
let sheet = arg(m, "sheet")
let text = @encoding/utf8.decode(@async/fs.read_file(input).binary())
let rows = parse_csv(text)
let workbook = @lib.new_workbook()
ignore(workbook.add_sheet(sheet))
for r in 0.. `rows` round-trip. Plain numbers are stored
// as real numeric cells.
set_cell_auto(
workbook,
sheet,
@lib.coordinates_to_cell_name(c + 1, r + 1),
cols[c],
)
}
}
write_file(output, @lib.write(workbook))
println("imported \{rows.length()} row(s) into \{output} (sheet \{sheet})")
}
///|
async fn run_view(m : @argparse.Matches) -> Unit {
let workbook = @lib.open_file(arg(m, "file"))
let sheet = resolve_sheet(workbook, arg(m, "sheet"))
println(render_ascii_table(workbook.get_rows(sheet)))
}
///|
async fn run_validate(m : @argparse.Matches) -> Unit {
// Validate the file's own bytes, not a re-serialized copy: opening and
// rewriting would hide input-package defects the reader tolerates.
let bytes = @async/fs.read_file(arg(m, "file")).binary()
let problems = @xl.validate_ooxml_package(bytes)
if problems.length() == 0 {
println("valid")
} else {
for problem in problems {
println(problem)
}
}
}
///|
async fn write_file(path : String, bytes : Bytes) -> Unit {
@async/fs.write_file(
path,
bytes,
create_mode=CreateOrTruncate,
permission=0o644,
)
}