use std::fs;
use crate::config::{Config, Delimiter};
use crate::util;
use crate::CliResult;
struct TempFileGuard(String);
impl Drop for TempFileGuard {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
static USAGE: &str = "
Formats CSV data with a custom delimiter or CRLF line endings.
Generally, all commands in xan output CSV data in a default format, which is
the same as the default format for reading CSV data. This makes it easy to
pipe multiple xan commands together. However, you may want the final result to
have a specific delimiter or record separator, and this is where 'xan fmt' is
useful.
Usage:
xan fmt [options] [<input>]
fmt options:
-i, --in-place Write the result in a temporary file and
replace input file with it when finished.
-t, --out-delimiter <arg> The field delimiter for writing CSV data.
[default: ,]
--crlf Use '\\r\\n' line endings in the output.
--ascii Use ASCII field and record separators.
--tabs Shorthand for -t '\\t'.
--quote <arg> The quote character to use. [default: \"]
--quote-always Put quotes around every value.
--quote-never Never put quotes around values, even if this would
produce invalid CSV data.
--escape <arg> The escape character to use. When not specified,
quotes are escaped by doubling them.
Common options:
-h, --help Display this message
-o, --output <file> Write output to <file> instead of stdout.
-d, --delimiter <arg> The field delimiter for reading CSV data.
Must be a single character.
";
#[derive(Deserialize)]
struct Args {
arg_input: Option<String>,
flag_in_place: bool,
flag_out_delimiter: Option<Delimiter>,
flag_crlf: bool,
flag_ascii: bool,
flag_tabs: bool,
flag_output: Option<String>,
flag_delimiter: Option<Delimiter>,
flag_quote: Delimiter,
flag_quote_always: bool,
flag_quote_never: bool,
flag_escape: Option<Delimiter>,
}
impl Args {
fn resolve(&mut self) -> CliResult<Option<TempFileGuard>> {
if self.flag_tabs {
self.flag_out_delimiter = Some(Delimiter(b'\t'));
}
if self.flag_in_place {
match &self.arg_input {
None => Err("-i/--in-place does not work with stdin!")?,
Some(p) if p.ends_with(".gz") => {
Err("-i/--in-place does not work with gzipped files!")?
}
Some(p) => {
if let Some(output) = &self.flag_output {
if output != p {
Err("-i/--in-place does not work if -o/--output is different than input!")?
}
}
let tmp_file_p = p.to_string() + ".xan-tmp";
self.flag_output = Some(tmp_file_p.clone());
return Ok(Some(TempFileGuard(tmp_file_p)));
}
};
}
Ok(None)
}
}
pub fn run(argv: &[&str]) -> CliResult<()> {
let mut args: Args = util::get_args(USAGE, argv)?;
let temp_file_guard_opt = args.resolve()?;
let rconfig = Config::new(&args.arg_input)
.delimiter(args.flag_delimiter)
.no_headers(true);
let mut wconfig = Config::new(&args.flag_output)
.delimiter(args.flag_out_delimiter)
.crlf(args.flag_crlf);
if args.flag_ascii {
wconfig = wconfig
.delimiter(Some(Delimiter(b'\x1f')))
.terminator(csv::Terminator::Any(b'\x1e'));
}
if args.flag_quote_always {
wconfig = wconfig.quote_style(csv::QuoteStyle::Always);
} else if args.flag_quote_never {
wconfig = wconfig.quote_style(csv::QuoteStyle::Never);
}
if let Some(escape) = args.flag_escape {
wconfig = wconfig.escape(Some(escape.as_byte())).double_quote(false);
}
wconfig = wconfig.quote(args.flag_quote.as_byte());
let mut rdr = rconfig.reader()?;
let mut wtr = wconfig.writer()?;
let mut record = csv::ByteRecord::new();
while rdr.read_byte_record(&mut record)? {
wtr.write_byte_record(&record)?;
}
wtr.flush()?;
if let Some(p) = &temp_file_guard_opt {
fs::rename(&p.0, args.arg_input.unwrap())?;
}
Ok(())
}