Skip to content

Commit 49135e4

Browse files
Stephan Heßelmann (lgtf/39809)pacman82
Stephan Heßelmann (lgtf/39809)
authored andcommittedApr 17, 2020
Make command line interface more explicit in regards to stdin/stdout.
1 parent 0a97b0f commit 49135e4

File tree

3 files changed

+130
-61
lines changed

3 files changed

+130
-61
lines changed
 

‎Readme.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Usage
2727
-----
2828

2929
```bash
30-
di-csv2xml Category -i input.csv -o output.xml
30+
di-csv2xml --category Category --input input.csv --output output.xml
3131
```
3232

3333
converts this `input.csv` file
@@ -58,6 +58,12 @@ into this `output.xml`:
5858
</Category>
5959
```
6060

61+
The shell's pipe functionality can be leveraged to produce the same result:
62+
63+
```bash
64+
cat input.csv | di-csv2xml --category Category --input - > output.xml
65+
```
66+
6167
Each line of the `input.csv` file is transformed into a separate XML-record. These are globally
6268
embedded into a root-tag structure specified by the parameter `Category`.
6369

@@ -90,12 +96,9 @@ As this tool does not provide any schema validation, it is important to note tha
9096
Any typo in the parameter `category` or the header column of the csv-file is directly translated into the
9197
dedicated XML-tag, leading to potential errors when attempting to process the XML-file further.
9298

93-
94-
![demo](./di-csv2xml-demo.gif)
95-
9699
Support
97100
-------
98101

99102
This tool is provided as is under an MIT license without any warranty or SLA. You are free to use
100103
it as part for any purpose, but the responsibility for operating it resides with you. We appreciate
101-
your feedback though. Contributions on GitHub are welcome.
104+
your feedback though. Contributions on GitHub are welcome.

‎src/main.rs

+98-52
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ use atty::{isnt, Stream};
77
use flate2::{bufread::GzDecoder, GzBuilder};
88
use indicatif::{ProgressBar, ProgressStyle};
99
use quicli::prelude::*;
10-
use std::{fs::File, io, path::Path};
10+
use std::{
11+
fs::File,
12+
io,
13+
path::{Path, PathBuf},
14+
str::FromStr,
15+
};
1116
use structopt::StructOpt;
1217
use strum;
1318

@@ -17,14 +22,14 @@ use strum;
1722
#[derive(Debug, StructOpt)]
1823
struct Cli {
1924
/// Root tag of generated XML.
20-
#[structopt()]
25+
#[structopt(long, short = "c")]
2126
category: String,
22-
/// Path to input file. If ommited STDIN is used for input.
23-
#[structopt(long, short = "i", parse(from_os_str))]
24-
input: Option<std::path::PathBuf>,
25-
/// Path to output file. If ommited output is written to STDOUT.
26-
#[structopt(long, short = "o", parse(from_os_str))]
27-
output: Option<std::path::PathBuf>,
27+
/// Path to input file. Set to "-" to use STDIN instead of a file.
28+
#[structopt(long, short = "i")]
29+
input: IoArg,
30+
/// Path to output file. Leave out or set to "-" to use STDOUT instead of a file.
31+
#[structopt(long, short = "o", default_value = "-")]
32+
output: IoArg,
2833
/// Record type of generated XML. Should be either Record, DeleteRecord, DeleteAllRecords.
2934
#[structopt(long = "record-type", short = "r", default_value = "Record")]
3035
record_type: RecordType,
@@ -34,6 +39,40 @@ struct Cli {
3439
delimiter: char,
3540
}
3641

42+
/// IO argument for CLI tools which can either take a file or STDIN/STDOUT.
43+
///
44+
/// Caveat: stdin is represented as "-" at the command line. Which means your tool is going to have
45+
/// a hard time operating on a file named "-".
46+
#[derive(Debug)]
47+
enum IoArg {
48+
/// Indicates that the IO is connected to stdin/stdout. Represented as a "-" on the command line.
49+
StdStream,
50+
/// Indicates that the IO is connected to a file. Contains the file path. Just enter a path
51+
/// at the command line.
52+
File(PathBuf),
53+
}
54+
55+
impl IoArg {
56+
fn is_file(&self) -> bool {
57+
match self {
58+
IoArg::StdStream => false,
59+
IoArg::File(_) => true,
60+
}
61+
}
62+
}
63+
64+
impl FromStr for IoArg {
65+
type Err = std::convert::Infallible;
66+
67+
fn from_str(s: &str) -> Result<Self, Self::Err> {
68+
let out = match s {
69+
"-" => IoArg::StdStream,
70+
other => IoArg::File(other.into()),
71+
};
72+
Ok(out)
73+
}
74+
}
75+
3776
fn main() -> CliResult {
3877
let args = Cli::from_args();
3978

@@ -54,61 +93,68 @@ fn main() -> CliResult {
5493
// file, we open in this code).
5594
let std_out;
5695

57-
let input: Box<dyn io::Read> = if let Some(input) = args.input {
58-
// Path argument specified. Open file and initialize progress bar.
59-
let file = File::open(&input)?;
60-
// Only show Progress bar, if input is a file and output is not /dev/tty.
61-
//
62-
// * We need the input to so we have the file metadata and therefore file length, to know
63-
// the amount of data we are going to proccess. Otherwise we can't set the length of the
64-
// progress bar.
65-
// * We don't want the Progress bar to interfere with the output, if writing to /dev/tty.
66-
// Progress bar interferes with formatting if stdout and stderr both go to /dev/tty
67-
if args.output.is_some() || isnt(Stream::Stdout) {
68-
let len = file.metadata()?.len();
69-
progress_bar = ProgressBar::new(len);
70-
let fmt = "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})";
71-
progress_bar.set_style(
72-
ProgressStyle::default_bar()
73-
.template(fmt)
74-
.progress_chars("#>-"),
75-
);
76-
let file_with_pbar = progress_bar.wrap_read(file);
77-
78-
if has_gz_extension(&input) {
79-
Box::new(GzDecoder::new(io::BufReader::new(file_with_pbar)))
80-
} else {
81-
Box::new(file_with_pbar)
82-
}
83-
} else {
84-
// Input file, but writing output to /dev/tty
96+
let input: Box<dyn io::Read> = match args.input {
97+
IoArg::File(input) => {
98+
// Path argument specified. Open file and initialize progress bar.
99+
let file = File::open(&input)?;
100+
// Only show Progress bar, if input is a file and output is not /dev/tty.
101+
//
102+
// * We need the input to so we have the file metadata and therefore file length, to know
103+
// the amount of data we are going to proccess. Otherwise we can't set the length of the
104+
// progress bar.
105+
// * We don't want the Progress bar to interfere with the output, if writing to /dev/tty.
106+
// Progress bar interferes with formatting if stdout and stderr both go to /dev/tty
107+
if args.output.is_file() || isnt(Stream::Stdout) {
108+
let len = file.metadata()?.len();
109+
progress_bar = ProgressBar::new(len);
110+
let fmt = "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})";
111+
progress_bar.set_style(
112+
ProgressStyle::default_bar()
113+
.template(fmt)
114+
.progress_chars("#>-"),
115+
);
116+
let file_with_pbar = progress_bar.wrap_read(file);
85117

86-
// Repeat if to avoid extra Box.
87-
if has_gz_extension(&input) {
88-
Box::new(GzDecoder::new(io::BufReader::new(file)))
118+
if has_gz_extension(&input) {
119+
Box::new(GzDecoder::new(io::BufReader::new(file_with_pbar)))
120+
} else {
121+
Box::new(file_with_pbar)
122+
}
89123
} else {
90-
Box::new(file)
124+
// Input file, but writing output to /dev/tty
125+
126+
// Repeat if to avoid extra Box.
127+
if has_gz_extension(&input) {
128+
Box::new(GzDecoder::new(io::BufReader::new(file)))
129+
} else {
130+
Box::new(file)
131+
}
91132
}
92133
}
93-
} else {
94-
// Input path not set => Just use stdin
95-
std_in = io::stdin();
96-
Box::new(std_in.lock())
134+
IoArg::StdStream => {
135+
// Input path not set => Just use stdin
136+
std_in = io::stdin();
137+
Box::new(std_in.lock())
138+
}
97139
};
98140

99141
let reader = CsvSource::new(input, args.delimiter as u8)?;
100142

101-
let mut out: Box<dyn io::Write> = if let Some(output) = args.output {
102-
let writer = io::BufWriter::new(File::create(&output)?);
143+
let mut out: Box<dyn io::Write> = match args.output {
144+
IoArg::File(output) => {
145+
let writer = io::BufWriter::new(File::create(&output)?);
103146

104-
if has_gz_extension(&output) {
105-
Box::new(GzBuilder::new().write(writer, Default::default()))
106-
} else {
147+
if has_gz_extension(&output) {
148+
Box::new(GzBuilder::new().write(writer, Default::default()))
149+
} else {
150+
Box::new(writer)
151+
}
152+
}
153+
IoArg::StdStream => {
154+
std_out = io::stdout();
155+
let writer = io::BufWriter::new(std_out.lock());
107156
Box::new(writer)
108157
}
109-
} else {
110-
std_out = io::stdout();
111-
Box::new(std_out.lock())
112158
};
113159
generate_xml(&mut out, reader, &args.category, args.record_type)?;
114160
Ok(())

‎tests/lib.rs

+24-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@ use tempfile::tempdir;
66
fn simple() {
77
Command::cargo_bin("di-csv2xml")
88
.unwrap()
9-
.args(&["Category", "--input", "tests/input.csv"])
9+
.args(&["--category", "Category", "--input", "tests/input.csv"])
10+
.assert()
11+
.success()
12+
.stdout(include_str!("output.xml").replace("\r\n", "\n"));
13+
}
14+
15+
#[test]
16+
fn simple_stdin() {
17+
Command::cargo_bin("di-csv2xml")
18+
.unwrap()
19+
.write_stdin(include_str!("input.csv").replace("\r\n", "\n"))
20+
.args(&["--category", "Category", "--input", "-"])
1021
.assert()
1122
.success()
1223
.stdout(include_str!("output.xml").replace("\r\n", "\n"));
@@ -16,7 +27,7 @@ fn simple() {
1627
fn input_gz() {
1728
Command::cargo_bin("di-csv2xml")
1829
.unwrap()
19-
.args(&["Category", "--input", "tests/input.csv.gz"])
30+
.args(&["--category", "Category", "--input", "tests/input.csv.gz"])
2031
.assert()
2132
.success()
2233
.stdout(include_str!("output.xml").replace("\r\n", "\n"));
@@ -26,7 +37,7 @@ fn input_gz() {
2637
fn mask_text() {
2738
Command::cargo_bin("di-csv2xml")
2839
.unwrap()
29-
.args(&["Text", "--input", "tests/text.csv"])
40+
.args(&["--category", "Text", "--input", "tests/text.csv"])
3041
.assert()
3142
.success()
3243
.stdout(include_str!("text.xml").replace("\r\n", "\n"));
@@ -37,6 +48,7 @@ fn semicolon_delimiter() {
3748
Command::cargo_bin("di-csv2xml")
3849
.unwrap()
3950
.args(&[
51+
"--category",
4052
"Category",
4153
"--input",
4254
"tests/sem_delim.csv",
@@ -53,6 +65,7 @@ fn delete_record() {
5365
Command::cargo_bin("di-csv2xml")
5466
.unwrap()
5567
.args(&[
68+
"--category",
5669
"Root",
5770
"--input",
5871
"tests/simple.csv",
@@ -69,6 +82,7 @@ fn delete_all() {
6982
Command::cargo_bin("di-csv2xml")
7083
.unwrap()
7184
.args(&[
85+
"--category",
7286
"Root",
7387
"--input",
7488
"tests/simple.csv",
@@ -84,7 +98,12 @@ fn delete_all() {
8498
fn customer_extensions() {
8599
Command::cargo_bin("di-csv2xml")
86100
.unwrap()
87-
.args(&["Root", "--input", "tests/customer_extensions.csv"])
101+
.args(&[
102+
"--category",
103+
"Root",
104+
"--input",
105+
"tests/customer_extensions.csv",
106+
])
88107
.assert()
89108
.success()
90109
.stdout(include_str!("customer_extensions.xml").replace("\r\n", "\n"));
@@ -100,6 +119,7 @@ fn write_gz() {
100119
Command::cargo_bin("di-csv2xml")
101120
.unwrap()
102121
.args(&[
122+
"--category",
103123
"Category",
104124
"--input",
105125
"tests/input.csv",

0 commit comments

Comments
 (0)
Please sign in to comment.