• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tamada / totebag / 13148839664

05 Feb 2025 02:17AM UTC coverage: 78.473% (-1.2%) from 79.706%
13148839664

push

github

web-flow
Merge pull request #46 from tamada/release/v0.7.6

Release/v0.7.6

117 of 179 new or added lines in 14 files covered. (65.36%)

1 existing line in 1 file now uncovered.

1644 of 2095 relevant lines covered (78.47%)

20.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.12
/src/cli.rs
1
use clap::{Parser, ValueEnum};
2
use std::{io::BufRead, path::PathBuf};
3

4
use totebag::{IgnoreType, Result, ToteError};
5

6
#[derive(Debug, Clone, ValueEnum, PartialEq, Copy)]
7
pub(crate) enum RunMode {
8
    Auto,
9
    Archive,
10
    Extract,
11
    List,
12
}
13

14
#[derive(Parser, Debug)]
15
#[clap(version, author, about, arg_required_else_help = true)]
16
pub(crate) struct CliOpts {
17
    #[clap(flatten)]
18
    pub extractors: ExtractorOpts,
19

20
    #[clap(flatten)]
21
    pub archivers: ArchiverOpts,
22

23
    #[clap(flatten)]
24
    pub listers: ListerOpts,
25

26
    #[clap(long = "log", help = "Specify the log level", default_value_t = LogLevel::Warn, ignore_case = true, value_enum)]
1✔
NEW
27
    pub loglevel: LogLevel,
×
28

29
    #[clap(short = 'm', long = "mode", default_value_t = RunMode::Auto, value_name = "MODE", required = false, ignore_case = true, value_enum, help = "Mode of operation.")]
1✔
30
    pub mode: RunMode,
×
31

32
    #[cfg(debug_assertions)]
33
    #[clap(
34
        long = "generate-completion",
35
        hide = true,
36
        help = "Generate the completion files"
37
    )]
38
    pub generate_completion: bool,
×
39

40
    #[clap(
41
        short = 'o',
42
        short_alias = 'd',
43
        long = "output",
44
        alias = "dest",
45
        value_name = "DEST",
46
        required = false,
47
        help = "Output file in archive mode, or output directory in extraction mode"
48
    )]
49
    pub output: Option<PathBuf>,
50

51
    #[clap(long, help = "Overwrite existing files.")]
52
    pub overwrite: bool,
×
53

54
    #[clap(
55
        value_name = "ARGUMENTS",
56
        help = r###"List of files or directories to be processed.
57
'-' reads form stdin, and '@<filename>' reads from a file.
58
In archive mode, the resultant archive file name is determined by the following rule.
59
    - if output option is specified, use it.
60
    - if the first argument is the archive file name, use it.
61
    - otherwise, use the default name 'totebag.zip'.
62
The format is determined by the extension of the resultant file name."###
63
    )]
64
    args: Vec<String>,
8✔
65
}
66

67
#[derive(Parser, Debug)]
68
pub struct ListerOpts {
69
    #[clap(
70
        short,
71
        long,
72
        help = "List entries in the archive file with long format."
73
    )]
74
    pub long: bool,
×
75
}
76

77
#[derive(Parser, Debug)]
78
pub struct ArchiverOpts {
79
    #[clap(
80
        short = 'C',
81
        long = "dir",
82
        value_name = "DIR",
83
        required = false,
84
        default_value = ".",
85
        help = "Specify the base directory for archiving or extracting."
86
    )]
87
    pub base_dir: PathBuf,
×
88

89
    #[clap(
90
        short = 'i',
91
        long = "ignore-types",
92
        value_name = "IGNORE_TYPES",
93
        value_delimiter = ',',
94
        help = "Specify the ignore type."
95
    )]
96
    pub ignores: Vec<IgnoreType>,
×
97

98
    #[clap(short = 'L', long = "level", default_value_t = 5, help = r#"Specify the compression level. [default: 5] [possible values: 0-9 (none to finest)]
1✔
99
For more details of level of each compression method, see README."#, value_parser=compression_level)]
NEW
100
    pub level: u8,
×
101

102
    #[clap(
103
        short = 'n',
104
        long = "no-recursive",
105
        help = "No recursive directory (archive mode).",
106
        default_value_t = false
1✔
107
    )]
108
    pub no_recursive: bool,
×
109
}
110

111
#[derive(Parser, Debug)]
112
pub struct ExtractorOpts {
113
    #[clap(
114
        long = "to-archive-name-dir",
115
        help = "extract files to DEST/ARCHIVE_NAME directory (extract mode).",
116
        default_value_t = false
1✔
117
    )]
118
    pub to_archive_name_dir: bool,
×
119
}
120

121
/// The log level.
122
#[derive(Parser, Debug, ValueEnum, Clone, PartialEq, Copy)]
123
pub enum LogLevel {
124
    /// The error level.
125
    Error,
126
    /// The warning level.
127
    Warn,
128
    /// The info level.
129
    Info,
130
    /// The debug level.
131
    Debug,
132
    /// The trace level.
133
    Trace,
134
}
135

136
fn compression_level(s: &str) -> core::result::Result<u8, String> {
9✔
137
    clap_num::number_range(s, 0, 9)
9✔
138
}
9✔
139

140
#[derive(Parser, Debug)]
141
struct ActualArgs {
142
    args: Vec<String>,
×
143
}
144

145
impl ActualArgs {}
146

147
impl CliOpts {
148
    pub fn args(&self) -> Vec<String> {
4✔
149
        self.args.clone()
4✔
150
    }
4✔
151

152
    pub fn run_mode(&self) -> RunMode {
7✔
153
        self.mode
7✔
154
    }
7✔
155

156
    pub fn archiver_output(&self) -> PathBuf {
×
157
        self.output
×
158
            .clone()
×
159
            .unwrap_or_else(|| PathBuf::from("totebag.zip"))
×
160
    }
×
161

162
    pub fn extractor_output(&self) -> PathBuf {
×
163
        self.output.clone().unwrap_or_else(|| PathBuf::from("."))
×
164
    }
×
165

166
    pub(crate) fn finalize(&mut self, m: &totebag::format::Manager) -> Result<()> {
7✔
167
        let args = match normalize_args(self.args.clone()) {
7✔
168
            Ok(args) => args,
7✔
169
            Err(e) => return Err(e),
×
170
        };
171
        if args.is_empty() {
7✔
172
            return Err(ToteError::NoArgumentsGiven);
×
173
        }
7✔
174
        if self.mode == RunMode::Auto {
7✔
175
            if m.match_all(&args) {
6✔
176
                self.args = args;
2✔
177
                self.mode = RunMode::Extract;
2✔
178
            } else {
2✔
179
                self.mode = RunMode::Archive;
4✔
180
                if m.find(&args[0]).is_some() && self.output.is_none() {
4✔
181
                    self.output = Some(args[0].clone().into());
1✔
182
                    self.args = args[1..].to_vec();
1✔
183
                } else {
3✔
184
                    self.args = args;
3✔
185
                }
3✔
186
            }
187
        } else {
1✔
188
            self.args = args;
1✔
189
        }
1✔
190
        Ok(())
7✔
191
    }
7✔
192
}
193

194
pub(crate) fn normalize_args(args: Vec<String>) -> Result<Vec<String>> {
7✔
195
    let results = args
7✔
196
        .iter()
7✔
197
        .map(reads_file_or_stdin_if_needed)
7✔
198
        .collect::<Vec<Result<Vec<String>>>>();
7✔
199
    if results.iter().any(|r| r.is_err()) {
19✔
200
        let errs = results
×
201
            .into_iter()
×
202
            .filter(|r| r.is_err())
×
203
            .flat_map(|r| r.err())
×
204
            .collect::<Vec<ToteError>>();
×
205
        Err(ToteError::Array(errs))
×
206
    } else {
207
        let results = results
7✔
208
            .into_iter()
7✔
209
            .filter(|r| r.is_ok())
19✔
210
            .flat_map(|r| r.unwrap())
19✔
211
            .collect::<Vec<String>>();
7✔
212
        Ok(results)
7✔
213
    }
214
}
7✔
215

216
fn reads_file_or_stdin_if_needed<S: AsRef<str>>(s: S) -> Result<Vec<String>> {
19✔
217
    let s = s.as_ref();
19✔
218
    if s == "-" {
19✔
219
        reads_from_reader(std::io::stdin())
×
220
    } else if let Some(stripped_str) = s.strip_prefix('@') {
19✔
221
        reads_from_file(stripped_str)
3✔
222
    } else {
223
        Ok(vec![s.to_string()])
16✔
224
    }
225
}
19✔
226

227
fn reads_from_file<S: AsRef<str>>(s: S) -> Result<Vec<String>> {
3✔
228
    match std::fs::File::open(s.as_ref()) {
3✔
229
        Ok(f) => reads_from_reader(f),
3✔
230
        Err(e) => Err(ToteError::IO(e)),
×
231
    }
232
}
3✔
233

234
fn reads_from_reader<R: std::io::Read>(r: R) -> Result<Vec<String>> {
3✔
235
    let results = std::io::BufReader::new(r)
3✔
236
        .lines()
3✔
237
        .map_while(|r| r.ok())
23✔
238
        .map(|line| line.trim().to_string())
23✔
239
        .filter(|line| !line.is_empty() && !line.starts_with('#'))
23✔
240
        .collect::<Vec<String>>();
3✔
241
    Ok(results)
3✔
242
}
3✔
243

244
#[cfg(test)]
245
mod tests {
246
    use super::*;
247
    use clap::Parser;
248

249
    #[test]
250
    fn test_read_from_file1() {
1✔
251
        let manager = totebag::format::Manager::default();
1✔
252
        let mut cli = CliOpts::parse_from(&["totebag_test", "@testdata/files/archive_mode1.txt"]);
1✔
253
        match cli.finalize(&manager) {
1✔
254
            Ok(_) => {}
1✔
255
            Err(e) => panic!("error: {:?}", e),
×
256
        }
257
        assert_eq!(cli.run_mode(), RunMode::Archive);
1✔
258
        assert_eq!(
1✔
259
            cli.args(),
1✔
260
            vec!["src", "README.md", "LICENSE", "Cargo.toml", "Makefile.toml"]
1✔
261
        );
1✔
262
        assert_eq!(cli.output, Some(PathBuf::from("testdata/targets.tar.gz")));
1✔
263
    }
1✔
264

265
    #[test]
266
    fn test_read_from_file2() {
1✔
267
        let manager = totebag::format::Manager::default();
1✔
268
        let mut cli = CliOpts::parse_from(&["totebag_test", "@testdata/files/archive_mode2.txt"]);
1✔
269
        match cli.finalize(&manager) {
1✔
270
            Ok(_) => {}
1✔
271
            Err(e) => panic!("error: {:?}", e),
×
272
        }
273
        assert_eq!(cli.run_mode(), RunMode::Archive);
1✔
274
        assert_eq!(
1✔
275
            cli.args(),
1✔
276
            vec!["src", "README.md", "LICENSE", "Cargo.toml", "Makefile.toml"]
1✔
277
        );
1✔
278
        assert!(cli.output.is_none());
1✔
279
    }
1✔
280

281
    #[test]
282
    fn test_read_from_file3() {
1✔
283
        let manager = totebag::format::Manager::default();
1✔
284
        let mut cli = CliOpts::parse_from(&["totebag_test", "@testdata/files/extract_mode.txt"]);
1✔
285
        match cli.finalize(&manager) {
1✔
286
            Ok(_) => {}
1✔
287
            Err(e) => panic!("error: {:?}", e),
×
288
        }
289
        assert_eq!(cli.run_mode(), RunMode::Extract);
1✔
290
        assert_eq!(cli.args(), vec!["testdata/test.cab", "testdata/test.tar"]);
1✔
291
        assert!(cli.output.is_none());
1✔
292
    }
1✔
293

294
    #[test]
295
    fn test_find_mode_1() {
1✔
296
        let manager = totebag::format::Manager::default();
1✔
297
        let mut cli1 =
1✔
298
            CliOpts::parse_from(&["totebag_test", "src", "LICENSE", "README.md", "Cargo.toml"]);
1✔
299
        assert!(cli1.finalize(&manager).is_ok());
1✔
300
        assert_eq!(cli1.run_mode(), RunMode::Archive);
1✔
301
    }
1✔
302

303
    #[test]
304
    fn test_find_mode_2() {
1✔
305
        let manager = totebag::format::Manager::default();
1✔
306
        let mut cli2 =
1✔
307
            CliOpts::parse_from(&["totebag_test", "src", "LICENSE", "README.md", "hoge.zip"]);
1✔
308
        assert!(cli2.finalize(&manager).is_ok());
1✔
309
        assert_eq!(cli2.run_mode(), RunMode::Archive);
1✔
310
    }
1✔
311

312
    #[test]
313
    fn test_find_mode_3() {
1✔
314
        let manager = totebag::format::Manager::default();
1✔
315
        let mut cli3 = CliOpts::parse_from(&[
1✔
316
            "totebag_test",
1✔
317
            "src.zip",
1✔
318
            "LICENSE.tar",
1✔
319
            "README.tar.bz2",
1✔
320
            "hoge.rar",
1✔
321
        ]);
1✔
322
        assert!(cli3.finalize(&manager).is_ok());
1✔
323
        assert_eq!(cli3.run_mode(), RunMode::Extract);
1✔
324
    }
1✔
325

326
    #[test]
327
    fn test_find_mode_4() {
1✔
328
        let manager = totebag::format::Manager::default();
1✔
329
        let mut cli4 = CliOpts::parse_from(&[
1✔
330
            "totebag_test",
1✔
331
            "src.zip",
1✔
332
            "LICENSE.tar",
1✔
333
            "README.tar.bz2",
1✔
334
            "hoge.rar",
1✔
335
            "--mode",
1✔
336
            "list",
1✔
337
        ]);
1✔
338
        assert!(cli4.finalize(&manager).is_ok());
1✔
339
        assert_eq!(cli4.run_mode(), RunMode::List);
1✔
340
    }
1✔
341

342
    #[test]
343
    fn test_cli_parse_error() {
1✔
344
        let r = CliOpts::try_parse_from(&["totebag_test"]);
1✔
345
        assert!(r.is_err());
1✔
346
    }
1✔
347
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc