@@ -7,7 +7,12 @@ use atty::{isnt, Stream};
7
7
use flate2:: { bufread:: GzDecoder , GzBuilder } ;
8
8
use indicatif:: { ProgressBar , ProgressStyle } ;
9
9
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
+ } ;
11
16
use structopt:: StructOpt ;
12
17
use strum;
13
18
@@ -17,14 +22,14 @@ use strum;
17
22
#[ derive( Debug , StructOpt ) ]
18
23
struct Cli {
19
24
/// Root tag of generated XML.
20
- #[ structopt( ) ]
25
+ #[ structopt( long , short = "c" ) ]
21
26
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 ,
28
33
/// Record type of generated XML. Should be either Record, DeleteRecord, DeleteAllRecords.
29
34
#[ structopt( long = "record-type" , short = "r" , default_value = "Record" ) ]
30
35
record_type : RecordType ,
@@ -34,6 +39,40 @@ struct Cli {
34
39
delimiter : char ,
35
40
}
36
41
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
+
37
76
fn main ( ) -> CliResult {
38
77
let args = Cli :: from_args ( ) ;
39
78
@@ -54,61 +93,68 @@ fn main() -> CliResult {
54
93
// file, we open in this code).
55
94
let std_out;
56
95
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) ;
85
117
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
+ }
89
123
} 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
+ }
91
132
}
92
133
}
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
+ }
97
139
} ;
98
140
99
141
let reader = CsvSource :: new ( input, args. delimiter as u8 ) ?;
100
142
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) ?) ;
103
146
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 ( ) ) ;
107
156
Box :: new ( writer)
108
157
}
109
- } else {
110
- std_out = io:: stdout ( ) ;
111
- Box :: new ( std_out. lock ( ) )
112
158
} ;
113
159
generate_xml ( & mut out, reader, & args. category , args. record_type ) ?;
114
160
Ok ( ( ) )
0 commit comments