1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use std::{
fs::File,
io::{self, BufWriter},
path::PathBuf,
};
use anyhow::{anyhow, Context};
use clap::{ArgAction, Parser, ValueEnum};
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum OutputEncoding {
Infer,
Csv,
Json,
}
#[derive(Debug, Parser)]
#[clap(version, about)]
pub struct Args {
#[clap(
help = "A DBZ file to convert to another encoding. Pass '-' to read from standard input",
value_name = "FILE"
)]
pub input: PathBuf,
#[clap(
short,
long,
help = "Saves the result to FILE. If no path is specified, the output will be written to standard output",
value_name = "FILE"
)]
pub output: Option<PathBuf>,
#[clap(
short = 'J',
long,
action = ArgAction::SetTrue,
default_value = "false",
help = "Output the result as NDJSON (newline-delimited JSON)"
)]
pub json: bool,
#[clap(
short = 'C',
long,
action = ArgAction::SetTrue,
default_value = "false",
conflicts_with = "json",
help = "Output the result as CSV"
)]
pub csv: bool,
#[clap(
short,
long,
action = ArgAction::SetTrue,
default_value = "false",
help = "Allow overwriting of existing files, such as the output file"
)]
pub force: bool,
#[clap(
short = 'm',
long = "metadata",
action = ArgAction::SetTrue,
default_value = "false",
help = "Output the metadata section instead of the body of the DBZ file"
)]
pub should_output_metadata: bool,
#[clap(
short = 'p',
long = "pretty-json",
action = ArgAction::SetTrue,
default_value = "false",
help ="Make the JSON output easier to read with spacing and indentation"
)]
pub should_pretty_print: bool,
}
impl Args {
pub fn output_encoding(&self) -> OutputEncoding {
match (self.json, self.csv) {
(false, false) => OutputEncoding::Infer,
(true, false) => OutputEncoding::Json,
(false, true) => OutputEncoding::Csv,
(true, true) => unreachable!("Invalid state that clap conflicts_with should prevent"),
}
}
}
pub fn infer_encoding(args: &Args) -> anyhow::Result<dbz_lib::OutputEncoding> {
match args.output_encoding() {
OutputEncoding::Csv => Ok(dbz_lib::OutputEncoding::Csv),
OutputEncoding::Json => Ok(dbz_lib::OutputEncoding::Json {
should_pretty_print: args.should_pretty_print,
}),
OutputEncoding::Infer => match args.output.as_ref().and_then(|o| o.extension()) {
Some(ext) if ext == "csv" => Ok(dbz_lib::OutputEncoding::Csv),
Some(ext) if ext == "json" => Ok(dbz_lib::OutputEncoding::Json {
should_pretty_print: args.should_pretty_print,
}),
Some(ext) => Err(anyhow!(
"Unable to infer output encoding from output file with extension '{}'",
ext.to_string_lossy()
)),
None => Err(anyhow!(
"Unable to infer output encoding from output file without an extension"
)),
},
}
}
pub fn output_from_args(args: &Args) -> anyhow::Result<Box<dyn io::Write>> {
if let Some(output) = &args.output {
let output_file = open_output_file(output, args.force)?;
Ok(Box::new(BufWriter::new(output_file)))
} else {
Ok(Box::new(io::stdout().lock()))
}
}
fn open_output_file(path: &PathBuf, force: bool) -> anyhow::Result<File> {
let mut options = File::options();
options.write(true);
if force {
options.create(true);
} else if path.exists() {
return Err(anyhow!(
"Output file exists. Pass --force flag to overwrite the existing file."
));
} else {
options.create_new(true);
}
options
.open(path)
.with_context(|| format!("Unable to open output file '{}'", path.display()))
}