repo_description: The CSV magician
repo_topics: cli csv rust tsv
code: 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.simd_reader()?;
      let mut wtr = wconfig.writer()?;
      let mut record = simd_csv::ByteRecord::new();
  
      while rdr.read_byte_record(&mut record)? {
          wtr.write_record(&record)?;
      }
  
      wtr.flush()?;
  
      if let Some(p) = &temp_file_guard_opt {
          fs::rename(&p.0, args.arg_input.unwrap())?;
      }
  
      Ok(())
  }
  