Skip to content

Commit d441958

Browse files
committed
support for manual format specification, better autodetection in the cli
1 parent 010cd7e commit d441958

File tree

10 files changed

+241
-29
lines changed

10 files changed

+241
-29
lines changed

json/empty.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

json/single.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"onlyone":"highlander"}

src/format.rs

+37
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,43 @@ pub enum Format {
1717
Toml,
1818
}
1919

20+
pub const POSSIBLE_FORMATS: &[&str] = &["json", "toml"];
21+
22+
impl std::fmt::Display for Format {
23+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
24+
write!(
25+
f,
26+
"{}",
27+
match self {
28+
Format::Json => "json",
29+
Format::Toml => "toml",
30+
}
31+
)
32+
}
33+
}
34+
35+
#[derive(Debug)]
36+
pub enum ParseFormatError {
37+
NoSuchFormat(String),
38+
NoFormatProvided,
39+
}
40+
41+
impl FromStr for Format {
42+
type Err = ParseFormatError;
43+
44+
fn from_str(s: &str) -> Result<Self, ParseFormatError> {
45+
let s = s.trim().to_lowercase();
46+
47+
if s == "json" {
48+
Ok(Format::Json)
49+
} else if s == "toml" {
50+
Ok(Format::Toml)
51+
} else {
52+
Err(ParseFormatError::NoSuchFormat(s))
53+
}
54+
}
55+
}
56+
2057
impl Format {
2158
/// Generates a filesystem `fs`, reading from `reader` according to a
2259
/// particular `Config`.

src/fs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ impl FS {
184184
info!("called");
185185
debug!("{:?}", self.inodes);
186186

187-
if !self.synced.get() && !self.dirty.get() {
187+
if self.synced.get() && !self.dirty.get() {
188188
info!("skipping sync; already synced and not dirty");
189189
return;
190190
}

src/main.rs

+110-28
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::PathBuf;
33

44
use clap::{App, Arg};
55

6-
use tracing::{error, info, warn};
6+
use tracing::{debug, error, info, warn};
77
use tracing_subscriber::prelude::*;
88
use tracing_subscriber::{filter::EnvFilter, fmt};
99

@@ -103,6 +103,22 @@ fn main() {
103103
.overrides_with("OUTPUT")
104104
.overrides_with("NOOUTPUT")
105105
)
106+
.arg(
107+
Arg::with_name("SOURCE_FORMAT")
108+
.help("Specify the source format explicitly (by default, automatically inferred from filename extension)")
109+
.long("source")
110+
.short("s")
111+
.takes_value(true)
112+
.possible_values(format::POSSIBLE_FORMATS)
113+
)
114+
.arg(
115+
Arg::with_name("TARGET_FORMAT")
116+
.help("Specify the target format explicitly (by default, automatically inferred from filename extension)")
117+
.long("target")
118+
.short("t")
119+
.takes_value(true)
120+
.possible_values(format::POSSIBLE_FORMATS)
121+
)
106122
.arg(
107123
Arg::with_name("MOUNT")
108124
.help("Sets the mountpoint")
@@ -228,14 +244,90 @@ fn main() {
228244
} else {
229245
Output::Stdout
230246
};
231-
let reader: Box<dyn std::io::Read> = if input_source == "-" {
232-
Box::new(std::io::stdin())
233-
} else {
234-
let file = std::fs::File::open(input_source).unwrap_or_else(|e| {
235-
error!("Unable to open {} for JSON input: {}", input_source, e);
236-
std::process::exit(1);
237-
});
238-
Box::new(file)
247+
248+
// try to autodetect the input format.
249+
//
250+
// first see if it's specified and parses okay.
251+
//
252+
// then see if we can pull it out of the extension.
253+
//
254+
// then give up and use json
255+
config.input_format = match args
256+
.value_of("SOURCE_FORMAT")
257+
.ok_or(format::ParseFormatError::NoFormatProvided)
258+
.and_then(|s| s.parse::<Format>())
259+
{
260+
Ok(source_format) => source_format,
261+
Err(e) => {
262+
match e {
263+
format::ParseFormatError::NoSuchFormat(s) => {
264+
warn!("Unrecognized format '{}', inferring from input.", s)
265+
}
266+
format::ParseFormatError::NoFormatProvided => {
267+
debug!("Inferring format from input.")
268+
}
269+
};
270+
271+
match Path::new(input_source)
272+
.extension() // will fail for STDIN, no worries
273+
.map(|s| s.to_str().expect("utf8 filename").to_lowercase())
274+
{
275+
Some(s) => match s.parse::<Format>() {
276+
Ok(format) => format,
277+
Err(_) => {
278+
warn!("Unrecognized format {}, defaulting to JSON.", s);
279+
Format::Json
280+
}
281+
},
282+
None => Format::Json,
283+
}
284+
}
285+
};
286+
287+
// try to autodetect the input format.
288+
//
289+
// first see if it's specified and parses okay.
290+
//
291+
// then see if we can pull it out of the extension (if specified)
292+
//
293+
// then give up and use the input format
294+
config.output_format = match args
295+
.value_of("TARGET_FORMAT")
296+
.ok_or(format::ParseFormatError::NoFormatProvided)
297+
.and_then(|s| s.parse::<Format>())
298+
{
299+
Ok(target_format) => target_format,
300+
Err(e) => {
301+
match e {
302+
format::ParseFormatError::NoSuchFormat(s) => {
303+
warn!(
304+
"Unrecognized format '{}', inferring from input and output.",
305+
s
306+
)
307+
}
308+
format::ParseFormatError::NoFormatProvided => {
309+
debug!("Inferring output format from input.")
310+
}
311+
};
312+
313+
match args
314+
.value_of("OUTPUT")
315+
.and_then(|s| Path::new(s).extension())
316+
.and_then(|s| s.to_str())
317+
{
318+
Some(s) => match s.parse::<Format>() {
319+
Ok(format) => format,
320+
Err(_) => {
321+
warn!(
322+
"Unrecognized format {}, defaulting to input format '{}'.",
323+
s, config.input_format
324+
);
325+
config.input_format
326+
}
327+
},
328+
None => config.input_format,
329+
}
330+
}
239331
};
240332

241333
let mut options = vec![MountOption::FSName(input_source.into())];
@@ -246,26 +338,16 @@ fn main() {
246338
options.push(MountOption::RO);
247339
}
248340

249-
// try to autodetect the input format... poorly
250-
config.input_format = match Path::new(input_source)
251-
.extension()
252-
.map(|s| s.to_str().expect("utf8 filename").to_lowercase())
253-
{
254-
Some(s) => {
255-
if &s == "json" {
256-
Format::Json
257-
} else if &s == "toml" {
258-
Format::Toml
259-
} else {
260-
warn!("Unrecognized format {}, defaulting to JSON.", s);
261-
Format::Json
262-
}
263-
}
264-
None => Format::Json,
265-
};
266-
config.output_format = config.input_format;
267-
268341
let input_format = config.input_format;
342+
let reader: Box<dyn std::io::Read> = if input_source == "-" {
343+
Box::new(std::io::stdin())
344+
} else {
345+
let file = std::fs::File::open(input_source).unwrap_or_else(|e| {
346+
error!("Unable to open {} for JSON input: {}", input_source, e);
347+
std::process::exit(1);
348+
});
349+
Box::new(file)
350+
};
269351
let fs = input_format.load(reader, config);
270352

271353
info!("mounting on {:?} with options {:?}", mount_point, options);

tests/json_to_toml.sh

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/sh
2+
3+
fail() {
4+
echo FAILED: $1
5+
if [ "$MNT" ]
6+
then
7+
cd
8+
umount "$MNT"
9+
rmdir "$MNT"
10+
rm "$TGT"
11+
fi
12+
exit 1
13+
}
14+
15+
MNT=$(mktemp -d)
16+
TGT=$(mktemp)
17+
18+
ffs --source json --target toml -o "$TGT" "$MNT" ../json/single.json &
19+
PID=$!
20+
sleep 2
21+
umount "$MNT" || fail unmount1
22+
sleep 1
23+
kill -0 $PID >/dev/null 2>&1 && fail process1
24+
25+
diff "$TGT" ../toml/single.toml || fail diff
26+
27+
rmdir "$MNT" || fail mount
28+
rm "$TGT"
29+

tests/override_infer.sh

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/sh
2+
3+
fail() {
4+
echo FAILED: $1
5+
if [ "$MNT" ]
6+
then
7+
cd
8+
umount "$MNT"
9+
rmdir "$MNT"
10+
rm "$SRC" "$TGT"
11+
fi
12+
exit 1
13+
}
14+
15+
MNT=$(mktemp -d)
16+
SRC=$(mktemp)
17+
TGT=$(mktemp)
18+
19+
cp ../toml/single.toml "$SRC"
20+
21+
ffs --source toml --target json -o "$TGT" "$MNT" "$SRC" &
22+
PID=$!
23+
sleep 2
24+
umount "$MNT" || fail unmount1
25+
sleep 1
26+
kill -0 $PID >/dev/null 2>&1 && fail process1
27+
28+
diff "$TGT" ../json/single.json || fail diff
29+
30+
rmdir "$MNT" || fail mount
31+
rm "$SRC" "$TGT"
32+

tests/toml_to_json.sh

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/sh
2+
3+
fail() {
4+
echo FAILED: $1
5+
if [ "$MNT" ]
6+
then
7+
cd
8+
umount "$MNT"
9+
rmdir "$MNT"
10+
rm "$TGT"
11+
fi
12+
exit 1
13+
}
14+
15+
MNT=$(mktemp -d)
16+
TGT=$(mktemp)
17+
18+
ffs --source toml --target json -o "$TGT" "$MNT" ../toml/single.toml &
19+
PID=$!
20+
sleep 2
21+
umount "$MNT" || fail unmount1
22+
sleep 1
23+
kill -0 $PID >/dev/null 2>&1 && fail process1
24+
25+
diff "$TGT" ../json/single.json || fail diff
26+
27+
rmdir "$MNT" || fail mount
28+
rm "$TGT"
29+

toml/empty.toml

Whitespace-only changes.

toml/single.toml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
onlyone = "highlander"

0 commit comments

Comments
 (0)