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

tamada / totebag / 13129544891

04 Feb 2025 06:18AM UTC coverage: 79.706% (+1.5%) from 78.178%
13129544891

push

github

web-flow
Merge pull request #43 from tamada/release/v0.7.5

Release/v0.7.5

172 of 215 new or added lines in 7 files covered. (80.0%)

3 existing lines in 3 files now uncovered.

1571 of 1971 relevant lines covered (79.71%)

19.59 hits per line

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

82.18
/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 = "level", help = "Specify the log level", default_value_t = LogLevel::Warn, ignore_case = true, value_enum)]
1✔
27
    pub level: 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. '-' reads form stdin, and '@<filename>' reads from a file.
57
If archive mode, the archive file name can specify at the first argument.
58
If the frist argument was not the archive name, the default archive name `totebag.zip` is applied.
59
"###
60
    )]
61
    args: Vec<String>,
8✔
62
}
63

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

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

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

95
    #[clap(
96
        short = 'n',
97
        long = "no-recursive",
98
        help = "No recursive directory (archive mode).",
99
        default_value_t = false
1✔
100
    )]
101
    pub no_recursive: bool,
×
102
}
103

104
#[derive(Parser, Debug)]
105
pub struct ExtractorOpts {
106
    #[clap(
107
        long = "to-archive-name-dir",
108
        help = "extract files to DEST/ARCHIVE_NAME directory (extract mode).",
109
        default_value_t = false
1✔
110
    )]
111
    pub to_archive_name_dir: bool,
×
112
}
113

114
/// The log level.
115
#[derive(Parser, Debug, ValueEnum, Clone, PartialEq, Copy)]
116
pub enum LogLevel {
117
    /// The error level.
118
    Error,
119
    /// The warning level.
120
    Warn,
121
    /// The info level.
122
    Info,
123
    /// The debug level.
124
    Debug,
125
    /// The trace level.
126
    Trace,
127
}
128

129
#[derive(Parser, Debug)]
130
struct ActualArgs {
NEW
131
    args: Vec<String>,
×
132
}
133

134
impl ActualArgs {}
135

136
impl CliOpts {
137
    pub fn args(&self) -> Vec<String> {
4✔
138
        self.args.clone()
4✔
139
    }
4✔
140

141
    pub fn run_mode(&self) -> RunMode {
7✔
142
        self.mode
7✔
143
    }
7✔
144

NEW
145
    pub fn archiver_output(&self) -> PathBuf {
×
NEW
146
        self.output
×
NEW
147
            .clone()
×
NEW
148
            .unwrap_or_else(|| PathBuf::from("totebag.zip"))
×
NEW
149
    }
×
150

NEW
151
    pub fn extractor_output(&self) -> PathBuf {
×
NEW
152
        self.output.clone().unwrap_or_else(|| PathBuf::from("."))
×
NEW
153
    }
×
154

155
    pub(crate) fn finalize(&mut self, m: &totebag::format::Manager) -> Result<()> {
7✔
156
        let args = match normalize_args(self.args.clone()) {
7✔
157
            Ok(args) => args,
7✔
NEW
158
            Err(e) => return Err(e),
×
159
        };
160
        if args.is_empty() {
7✔
UNCOV
161
            return Err(ToteError::NoArgumentsGiven);
×
162
        }
7✔
163
        if self.mode == RunMode::Auto {
7✔
164
            if m.match_all(&args) {
6✔
165
                self.args = args;
2✔
166
                self.mode = RunMode::Extract;
2✔
167
            } else {
2✔
168
                self.mode = RunMode::Archive;
4✔
169
                if m.find(&args[0]).is_some() && self.output.is_none() {
4✔
170
                    self.output = Some(args[0].clone().into());
1✔
171
                    self.args = args[1..].to_vec();
1✔
172
                } else {
3✔
173
                    self.args = args;
3✔
174
                }
3✔
175
            }
176
        } else {
1✔
177
            self.args = args;
1✔
178
        }
1✔
179
        Ok(())
7✔
180
    }
7✔
181
}
182

183
pub(crate) fn normalize_args(args: Vec<String>) -> Result<Vec<String>> {
7✔
184
    let results = args
7✔
185
        .iter()
7✔
186
        .map(reads_file_or_stdin_if_needed)
7✔
187
        .collect::<Vec<Result<Vec<String>>>>();
7✔
188
    if results.iter().any(|r| r.is_err()) {
19✔
NEW
189
        let errs = results
×
NEW
190
            .into_iter()
×
NEW
191
            .filter(|r| r.is_err())
×
NEW
192
            .flat_map(|r| r.err())
×
NEW
193
            .collect::<Vec<ToteError>>();
×
NEW
194
        Err(ToteError::Array(errs))
×
195
    } else {
196
        let results = results
7✔
197
            .into_iter()
7✔
198
            .filter(|r| r.is_ok())
19✔
199
            .flat_map(|r| r.unwrap())
19✔
200
            .collect::<Vec<String>>();
7✔
201
        Ok(results)
7✔
202
    }
203
}
7✔
204

205
fn reads_file_or_stdin_if_needed<S: AsRef<str>>(s: S) -> Result<Vec<String>> {
19✔
206
    let s = s.as_ref();
19✔
207
    if s == "-" {
19✔
NEW
208
        reads_from_reader(std::io::stdin())
×
209
    } else if let Some(stripped_str) = s.strip_prefix('@') {
19✔
210
        reads_from_file(stripped_str)
3✔
211
    } else {
212
        Ok(vec![s.to_string()])
16✔
213
    }
214
}
19✔
215

216
fn reads_from_file<S: AsRef<str>>(s: S) -> Result<Vec<String>> {
3✔
217
    match std::fs::File::open(s.as_ref()) {
3✔
218
        Ok(f) => reads_from_reader(f),
3✔
NEW
219
        Err(e) => Err(ToteError::IO(e)),
×
220
    }
221
}
3✔
222

223
fn reads_from_reader<R: std::io::Read>(r: R) -> Result<Vec<String>> {
3✔
224
    let results = std::io::BufReader::new(r)
3✔
225
        .lines()
3✔
226
        .map_while(|r| r.ok())
23✔
227
        .map(|line| line.trim().to_string())
23✔
228
        .filter(|line| !line.is_empty() && !line.starts_with('#'))
23✔
229
        .collect::<Vec<String>>();
3✔
230
    Ok(results)
3✔
231
}
3✔
232

233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236
    use clap::Parser;
237

238
    #[test]
239
    fn test_read_from_file1() {
1✔
240
        let manager = totebag::format::Manager::default();
1✔
241
        let mut cli = CliOpts::parse_from(&["totebag_test", "@testdata/files/archive_mode1.txt"]);
1✔
242
        match cli.finalize(&manager) {
1✔
243
            Ok(_) => {}
1✔
NEW
244
            Err(e) => panic!("error: {:?}", e),
×
245
        }
246
        assert_eq!(cli.run_mode(), RunMode::Archive);
1✔
247
        assert_eq!(
1✔
248
            cli.args(),
1✔
249
            vec!["src", "README.md", "LICENSE", "Cargo.toml", "Makefile.toml"]
1✔
250
        );
1✔
251
        assert_eq!(cli.output, Some(PathBuf::from("testdata/targets.tar.gz")));
1✔
252
    }
1✔
253

254
    #[test]
255
    fn test_read_from_file2() {
1✔
256
        let manager = totebag::format::Manager::default();
1✔
257
        let mut cli = CliOpts::parse_from(&["totebag_test", "@testdata/files/archive_mode2.txt"]);
1✔
258
        match cli.finalize(&manager) {
1✔
259
            Ok(_) => {}
1✔
NEW
260
            Err(e) => panic!("error: {:?}", e),
×
261
        }
262
        assert_eq!(cli.run_mode(), RunMode::Archive);
1✔
263
        assert_eq!(
1✔
264
            cli.args(),
1✔
265
            vec!["src", "README.md", "LICENSE", "Cargo.toml", "Makefile.toml"]
1✔
266
        );
1✔
267
        assert!(cli.output.is_none());
1✔
268
    }
1✔
269

270
    #[test]
271
    fn test_read_from_file3() {
1✔
272
        let manager = totebag::format::Manager::default();
1✔
273
        let mut cli = CliOpts::parse_from(&["totebag_test", "@testdata/files/extract_mode.txt"]);
1✔
274
        match cli.finalize(&manager) {
1✔
275
            Ok(_) => {}
1✔
NEW
276
            Err(e) => panic!("error: {:?}", e),
×
277
        }
278
        assert_eq!(cli.run_mode(), RunMode::Extract);
1✔
279
        assert_eq!(cli.args(), vec!["testdata/test.cab", "testdata/test.tar"]);
1✔
280
        assert!(cli.output.is_none());
1✔
281
    }
1✔
282

283
    #[test]
284
    fn test_find_mode_1() {
1✔
285
        let manager = totebag::format::Manager::default();
1✔
286
        let mut cli1 =
1✔
287
            CliOpts::parse_from(&["totebag_test", "src", "LICENSE", "README.md", "Cargo.toml"]);
1✔
288
        assert!(cli1.finalize(&manager).is_ok());
1✔
289
        assert_eq!(cli1.run_mode(), RunMode::Archive);
1✔
290
    }
1✔
291

292
    #[test]
293
    fn test_find_mode_2() {
1✔
294
        let manager = totebag::format::Manager::default();
1✔
295
        let mut cli2 =
1✔
296
            CliOpts::parse_from(&["totebag_test", "src", "LICENSE", "README.md", "hoge.zip"]);
1✔
297
        assert!(cli2.finalize(&manager).is_ok());
1✔
298
        assert_eq!(cli2.run_mode(), RunMode::Archive);
1✔
299
    }
1✔
300

301
    #[test]
302
    fn test_find_mode_3() {
1✔
303
        let manager = totebag::format::Manager::default();
1✔
304
        let mut cli3 = CliOpts::parse_from(&[
1✔
305
            "totebag_test",
1✔
306
            "src.zip",
1✔
307
            "LICENSE.tar",
1✔
308
            "README.tar.bz2",
1✔
309
            "hoge.rar",
1✔
310
        ]);
1✔
311
        assert!(cli3.finalize(&manager).is_ok());
1✔
312
        assert_eq!(cli3.run_mode(), RunMode::Extract);
1✔
313
    }
1✔
314

315
    #[test]
316
    fn test_find_mode_4() {
1✔
317
        let manager = totebag::format::Manager::default();
1✔
318
        let mut cli4 = CliOpts::parse_from(&[
1✔
319
            "totebag_test",
1✔
320
            "src.zip",
1✔
321
            "LICENSE.tar",
1✔
322
            "README.tar.bz2",
1✔
323
            "hoge.rar",
1✔
324
            "--mode",
1✔
325
            "list",
1✔
326
        ]);
1✔
327
        assert!(cli4.finalize(&manager).is_ok());
1✔
328
        assert_eq!(cli4.run_mode(), RunMode::List);
1✔
329
    }
1✔
330

331
    #[test]
332
    fn test_cli_parse_error() {
1✔
333
        let r = CliOpts::try_parse_from(&["totebag_test"]);
1✔
334
        assert!(r.is_err());
1✔
335
    }
1✔
336
}
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