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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
use clap::builder::TypedValueParser;
use clap::{Arg, Command, Error, ErrorKind, Parser, PossibleValue};
use std::ffi::OsStr;
use std::str::FromStr;
#[derive(Parser, Clone)]
pub(crate) struct TomlOpts {
/// jq filter to pass to `jq` binary.
///
/// Example: `'.dependencies'`.
#[clap(index = 1)]
pub(crate) jq_filter: Option<String>,
/// Special operation mode, takes JSON input and outputs TOML.
#[clap(short = 'T', long = "tomlit")]
pub(crate) toml_it: bool,
/// Show a cute bat output, with paging and highlight.
///
/// Needs to have `bat` installed, if `-R` is used, `bat` is launched with `toml` highlight
/// and will imply `-B` (beautify), otherwise it is launched with `json` highlight with pretty-printing,
/// unless `-c` (compact mode) is provided in *jq_args*.
///
/// `bat` will be launched using the `--paging=always` mode, so piping though it using this option
/// is not possible. If what you really want is to pipe it to `bat` (maybe to render an HTML page
/// with highlights using `aha`, for example), you should pipe it directly using the shell.
/// This option is for launching `bat` with paging and highlight by default.
#[clap(long)]
pub(crate) bat: bool,
/// Takes the output of `jq`, transcode back to TOML and output it (read about pretty print).
#[clap(short = 'R', long)]
pub(crate) retranscode: bool,
/// Skips invalid toml documents when re-transcoding from multi-document streams.
#[clap(short = '!', long)]
pub(crate) skip_invalid: bool,
/// Forces to treat the input as a single document.
///
/// tomq always tries to parse the input as a single document first, then
/// as a multi-document.
#[clap(short = '1', long)]
pub(crate) single_doc: bool,
/// Convert null json values to empty documents.
///
/// When re-transcoding into Toml, there are cases that *jq* outputs JSON
/// null values, because it cannot match the filter. In those cases, *tomq*
/// just fails, this switches to producing empty documents using the *multi-doc separator*.
///
/// Even though it would be interesting to see which documents are not matching the filter,
/// *tomq* uses streaming to read from multiple files and pipe to *jq*, and it reads from *jq*
/// output in a separated thread (unless the feature is disable at build-time), so the error may
/// happen while *jq* is already consuming any of the next documents.
///
/// The only way to deal with this is to process one document at a time, which is way slower.
#[clap(short = 'E', long = "null-to-empty-doc")]
pub(crate) null_to_empty_doc: bool,
/// The multi-document separator.
///
/// Note that, TOML, by specification, does not support multi documents,
/// this acts like an extension to the format. This was only added to be more
/// in line with jq tool, which supports reading and writing multiple json documents.
///
/// `---` is chosen as it does not conflicts with TOML format and can be safely handled
/// without disturbing an individual document (whether it is inside a stream with multiple
/// documents or not), but one can provide a custom separator.
///
/// Read more in this issue: https://github.com/toml-lang/toml/issues/583
#[clap(short = 'S', long, default_value = "---")]
pub(crate) multi_doc_separator: String,
/// The root key to use to produce multi-document output.
///
/// By default, it flattens the output to a non-compliant TOML format,
/// using the `multi-doc-separator` to separate each document. Although one
/// can provide a custom string like `documents` to produce a compliant TOML format
/// which uses the configured value as the root key for all documents.
///
/// It is important to note that the root key is used only for outputting the TOML,
/// for reading, you can feed the TOML back to tomq and use a jq filter to create multi-documents
/// back.
#[clap(short = 'U', long, value_parser = RootKeyParser, default_value = "flatten")]
pub(crate) root_key: RootKey,
/// Fallback key to use when the value is not an object.
///
/// Some jq filter can output basic types like strings, numbers, arrays, etc,
/// those cannot be directly converted to TOML, they need to be value of a field,
/// this option defines the fallback key to use as a field for those values..
#[clap(short = 'K', long)]
pub(crate) fallback_key: Option<String>,
/// Pretty print the output (always enabled for ttys).
///
/// This can be useful when piping to other tools like bat (https://github.com/sharkdp/bat).
///
/// There is no way to disable it for ttys (or tools that acts like ttys), to workaround
/// this, you can pipe the output to `cat`.
#[clap(short = 'B', long = "pp")]
pub(crate) pretty_print: bool,
/// Additional arguments to pass to *jq*.
///
/// You can use `--` to separate jq arguments from the tomq arguments (but it is not required),
/// for example: `tomq --pp --input-files nightly.toml -- -c`.
#[clap(last = true, index = 3, allow_hyphen_values = true, multiple = true)]
pub(crate) jq_args: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) enum RootKey {
Flatten,
Key(String),
}
#[derive(Clone, Copy)]
struct RootKeyParser;
const VARIANTS: &[&str] = &["flatten", "string"];
impl TypedValueParser for RootKeyParser {
type Value = RootKey;
fn parse_ref(
&self,
cmd: &Command,
arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, Error> {
let value = value.to_str().ok_or_else(|| {
invalid_value(
cmd,
value.to_string_lossy().into_owned(),
arg.map(ToString::to_string)
.unwrap_or_else(|| "...".to_owned()),
"",
)
})?;
let value = value.parse::<Self::Value>().map_err(|e| {
invalid_value(
cmd,
value.to_owned(),
arg.map(ToString::to_string)
.unwrap_or_else(|| "...".to_owned()),
&format!("Parse error: {}.", e),
)
})?;
Ok(value)
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue<'static>> + '_>> {
Some(Box::new(
VARIANTS.iter().filter_map(|v| Some(PossibleValue::new(v))),
))
}
}
fn invalid_value(cmd: &Command, bad_val: String, arg: String, ctx: &str) -> Error {
Error::raw(ErrorKind::InvalidValue, format!(
"{ctx}bad value `{bad_val}` provided to argument `{arg}` of command `{cmd}`. You should use `flatten` (or `\\flatten` to escape it as a key) or a valid UTF-8 string.",
bad_val = bad_val,
arg = arg,
cmd = cmd.get_name(),
))
}
impl FromStr for RootKey {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"flatten" => Ok(RootKey::Flatten),
"\\flatten" => Ok(RootKey::Key(s[1..].to_string())),
_ => Ok(RootKey::Key(wrap_key_if_needed(s))),
}
}
}
fn wrap_key_if_needed(s: &str) -> String {
for c in s.chars() {
if !(c.is_alphabetic() || c.is_numeric() || c == '_' || c == '-') {
return format!("{:?}", s);
}
}
return s.to_string();
}