static USAGE: &str = r#" Formats CSV data with a custom delimiter or CRLF line endings. Generally, all commands in qsv 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 qsv commands together. However, you may want the final result to have a specific delimiter or record separator, and this is where 'qsv fmt' is useful. For examples, see https://github.com/dathere/qsv/blob/master/tests/test_fmt.rs. Usage: qsv fmt [options] [] qsv fmt --help fmt options: -t, --out-delimiter The field delimiter for writing CSV data. Must be a single character. If set to "T", uses tab as the delimiter. [default: ,] --crlf Use '\r\n' line endings in the output. --ascii Use ASCII field and record separators. Use Substitute (U+00A1) as the quote character. --quote The quote character to use. [default: "] --quote-always Put quotes around every value. --quote-never Never put quotes around any value. --escape The escape character to use. When not specified, quotes are escaped by doubling them. --no-final-newline Do not write a newline at the end of the output. This makes it easier to paste the output into Excel. Common options: -h, --help Display this message -o, --output Write output to instead of stdout. -d, --delimiter The field delimiter for reading CSV data. Must be a single character. (default: ,) "#; use serde::Deserialize; use crate::{ CliResult, config::{Config, Delimiter}, util, }; #[derive(Deserialize)] struct Args { arg_input: Option, flag_out_delimiter: Option, flag_crlf: bool, flag_ascii: bool, flag_output: Option, flag_delimiter: Option, flag_quote: Delimiter, flag_quote_always: bool, flag_quote_never: bool, flag_escape: Option, flag_no_final_newline: bool, } pub fn run(argv: &[&str]) -> CliResult<()> { let mut args: Args = util::get_args(USAGE, argv)?; if args.flag_out_delimiter == Some(Delimiter(b'T')) { args.flag_out_delimiter = Some(Delimiter(b'\t')); } let rconfig = Config::new(args.arg_input.as_ref()) .delimiter(args.flag_delimiter) .no_headers(true); let mut wconfig = Config::new(args.flag_output.as_ref()) .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')); args.flag_quote = Delimiter(b'\x1a'); } 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 wsconfig = (wconfig).clone(); wsconfig.path = Some( tempfile::NamedTempFile::new()? .into_temp_path() .to_path_buf(), ); let mut temp_writer = wsconfig.writer()?; let mut current_record = csv::ByteRecord::new(); let mut next_record = csv::ByteRecord::new(); let mut is_last_record; let mut records_exist = rdr.read_byte_record(&mut current_record)?; while records_exist { is_last_record = !rdr.read_byte_record(&mut next_record)?; if is_last_record { // If it's the last record and the --no-final-newline flag is set, // write the record to a temporary file, then read the file into a string. // Remove the last character (the newline) from the string, then write the string to the // output. temp_writer.write_record(¤t_record)?; temp_writer.flush()?; let mut temp_string = match std::fs::read_to_string( wsconfig.path.as_ref().ok_or("Temp file path not found")?, ) { Ok(s) => s, Err(e) => return fail_clierror!("Error reading from temp file: {}", e), }; if args.flag_no_final_newline { temp_string.pop(); } match wtr.into_inner() { Ok(mut writer) => writer.write_all(temp_string.as_bytes())?, Err(e) => return fail_clierror!("Error writing to output: {}", e), } break; } wtr.write_record(¤t_record)?; wtr.write_record(&next_record)?; records_exist = rdr.read_byte_record(&mut current_record)?; } // we don't flush the writer explicitly // because it will cause a borrow error // let's just let it drop and flush itself implicitly Ok(()) }