ds/
report.rs

1#[cfg(test)]
2use clap::App;
3use clap::ArgMatches;
4extern crate colored;
5use self::colored::*;
6use std::collections::BTreeMap;
7use std::env;
8use std::io;
9#[allow(unused_imports)] // method write_all is needed
10use std::io::Write;
11use std::iter::FromIterator;
12
13pub struct ReportSettings {
14    pub all: bool,
15    pub reverse: bool,
16    pub lines: usize,
17    pub exclude: Vec<String>,
18}
19
20impl ReportSettings {
21    pub fn new() -> ReportSettings {
22        ReportSettings {
23            all: false,
24            reverse: false,
25            lines: 20,
26            exclude: Vec::new(),
27        }
28    }
29
30    pub fn settings(&mut self, matches: &ArgMatches) {
31        self.all = matches.occurrences_of("all") > 0;
32        self.reverse = matches.occurrences_of("reverse") > 0;
33
34        if let Some(lines) = matches.value_of("lines") {
35            self.lines = match lines.to_string().parse() {
36                Err(err) => {
37                    eprintln!("Check lines option: {}", err);
38                    self.lines
39                }
40                Ok(lines) => lines,
41            }
42        }
43
44        if let Some(exclude) = matches.values_of("exclude") {
45            self.exclude = exclude.map(|x| x.to_string()).collect();
46        }
47    }
48}
49
50/// Report
51///
52/// Send report to stdout
53pub fn report(disk_space: BTreeMap<String, u64>, matches: &ArgMatches) {
54    report_stream(&mut io::stdout(), disk_space, matches)
55}
56
57/// Report_Stream
58///
59/// Sort the entries by size and output the top 20
60#[allow(unused_must_use)]
61pub fn report_stream(
62    out: &mut io::Write,
63    mut disk_space: BTreeMap<String, u64>,
64    matches: &ArgMatches,
65) {
66    let mut rs = ReportSettings::new();
67    rs.settings(matches);
68    if !rs.exclude.is_empty() {
69        disk_space = exclude(&rs, disk_space);
70    }
71
72    let mut unsorted = Vec::from_iter(disk_space);
73    let end = endpoint(&rs, unsorted.len());
74
75    let sorted = if rs.reverse {
76        unsorted.sort_by(|&(_, a), &(_, b)| a.cmp(&b));
77        &unsorted[(unsorted.len() - end)..]
78    } else {
79        unsorted.sort_by(|&(_, a), &(_, b)| b.cmp(&a));
80        &unsorted[0..end]
81    };
82
83    for &(ref filename, size) in sorted {
84        writeln!(out, "{} {}", color(size, matches), filename);
85    }
86}
87
88fn endpoint(rs: &ReportSettings, length: usize) -> usize {
89    if !rs.all && length > rs.lines {
90        rs.lines
91    } else {
92        length
93    }
94}
95
96fn exclude(rs: &ReportSettings, disk_space: BTreeMap<String, u64>) -> BTreeMap<String, u64> {
97    let mut tmp = BTreeMap::new();
98    let mut include = true;
99    for filename in disk_space.keys() {
100        for exclusion in &rs.exclude {
101            if filename.contains(exclusion) {
102                include = false;
103                break;
104            }
105        }
106        if include {
107            tmp.insert(filename.to_string(), *disk_space.get(filename).unwrap());
108        } else {
109            include = true;
110        }
111    }
112    tmp
113}
114
115/// Color
116///
117/// Returns a string that will contain colored unit output if the
118/// TERM environment variable is set.  Defaults to yellow on Linux and
119/// cyan on Windows(cygwin).  Color preference specified as a command
120/// line option.
121fn color(number: u64, matches: &ArgMatches) -> String {
122    match env::var_os("TERM") {
123        None => simple_units(number),
124        Some(term) => match term.as_os_str().to_str().unwrap() {
125            "cygwin" => simple_units(number).cyan().bold().to_string(),
126            _ => match matches.value_of("color") {
127                Some("black") => simple_units(number).black().bold().to_string(),
128                Some("red") => simple_units(number).red().bold().to_string(),
129                Some("green") => simple_units(number).green().bold().to_string(),
130                Some("yellow") => simple_units(number).yellow().bold().to_string(),
131                Some("blue") => simple_units(number).blue().bold().to_string(),
132                Some("magenta") => simple_units(number).magenta().bold().to_string(),
133                Some("cyan") => simple_units(number).cyan().bold().to_string(),
134                Some("white") => simple_units(number).white().bold().to_string(),
135                Some("none") => simple_units(number),
136                _ => simple_units(number).yellow().bold().to_string(),
137            },
138        },
139    }
140}
141
142/// Simple_Units
143///
144/// Convert number to human friendly format
145fn simple_units(number: u64) -> String {
146    let units = [" ", "K", "M", "G", "T", "P"];
147    let index: usize = (number as f64).log(1024.0).trunc() as usize;
148    let n = number / 1024u64.pow(index as u32);
149
150    if index == 0 || index > 5 {
151        format!("{:>6}", n)
152    } else {
153        format!("{:>5}{}", n, units[index])
154    }
155}
156
157#[cfg(test)]
158#[allow(unused_must_use)]
159mod tests {
160    use super::*;
161    use clap::Arg;
162    use std::env;
163
164    #[cfg(target_os = "linux")]
165    #[test]
166    fn report_short() {
167        let mut data = BTreeMap::new();
168        data.insert("path/to/fileA".to_string(), 2048 as u64);
169        data.insert("path/to/fileB".to_string(), 1024 as u64);
170
171        let mut out = Vec::new();
172        let matches = App::new("DiskSpace").get_matches();
173        report_stream(&mut out, data, &matches);
174        assert_eq!(
175            out,
176            format!(
177                "{} path/to/fileA\n{} path/to/fileB\n",
178                "    2K".yellow().bold(),
179                "    1K".yellow().bold()
180            )
181            .as_bytes()
182        )
183    }
184
185    #[cfg(target_os = "linux")]
186    #[test]
187    fn report_short_reverse() {
188        let mut data = BTreeMap::new();
189        data.insert("path/to/fileA".to_string(), 2048 as u64);
190        data.insert("path/to/fileB".to_string(), 1024 as u64);
191
192        let mut out = Vec::new();
193        let args = vec!["ds", "-r"];
194        let matches = App::new("DiskSpace")
195            .arg(Arg::with_name("reverse").short("r"))
196            .get_matches_from(args);
197        report_stream(&mut out, data, &matches);
198        assert_eq!(
199            out,
200            format!(
201                "{} path/to/fileB\n{} path/to/fileA\n",
202                "    1K".yellow().bold(),
203                "    2K".yellow().bold(),
204            )
205            .as_bytes()
206        )
207    }
208
209    #[cfg(target_os = "linux")]
210    #[test]
211    fn report_short_exclude() {
212        let mut data = BTreeMap::new();
213        data.insert("path/to/fileA".to_string(), 2048 as u64);
214        data.insert("path/to/fileB".to_string(), 1024 as u64);
215
216        let mut out = Vec::new();
217        let args = vec!["ds", "-e", "fileB"];
218        let matches = App::new("DiskSpace")
219            .arg(
220                Arg::with_name("exclude")
221                    .short("e")
222                    .min_values(1)
223                    .multiple(true),
224            )
225            .get_matches_from(args);
226        report_stream(&mut out, data, &matches);
227        assert_eq!(
228            out,
229            format!("{} path/to/fileA\n", "    2K".yellow().bold(),).as_bytes()
230        )
231    }
232
233    #[cfg(target_os = "linux")]
234    #[test]
235    fn report_stdout() {
236        let data = BTreeMap::new();
237        let matches = App::new("DiskSpace").get_matches();
238        report(data, &matches);
239    }
240
241    #[cfg(target_os = "linux")]
242    #[test]
243    fn report_long() {
244        let mut data = BTreeMap::new();
245        data.insert("path/to/fileA".to_string(), 2048 as u64);
246        data.insert("path/to/fileB".to_string(), 1024 as u64);
247        data.insert("path/to/fileC".to_string(), 1023 as u64);
248        data.insert("path/to/fileD".to_string(), 1022 as u64);
249        data.insert("path/to/fileE".to_string(), 1021 as u64);
250        data.insert("path/to/fileF".to_string(), 1020 as u64);
251        data.insert("path/to/fileG".to_string(), 1019 as u64);
252        data.insert("path/to/fileH".to_string(), 1018 as u64);
253        data.insert("path/to/fileI".to_string(), 1017 as u64);
254        data.insert("path/to/fileJ".to_string(), 1016 as u64);
255        data.insert("path/to/fileK".to_string(), 1015 as u64);
256        data.insert("path/to/fileL".to_string(), 1014 as u64);
257        data.insert("path/to/fileM".to_string(), 1013 as u64);
258        data.insert("path/to/fileN".to_string(), 1012 as u64);
259        data.insert("path/to/fileO".to_string(), 1011 as u64);
260        data.insert("path/to/fileP".to_string(), 1010 as u64);
261        data.insert("path/to/fileQ".to_string(), 1009 as u64);
262        data.insert("path/to/fileR".to_string(), 1008 as u64);
263        data.insert("path/to/fileS".to_string(), 1007 as u64);
264        data.insert("path/to/fileT".to_string(), 1006 as u64);
265        data.insert("path/to/fileU".to_string(), 1005 as u64);
266
267        let mut out = Vec::new();
268        let matches = App::new("DiskSpace").get_matches();
269        report_stream(&mut out, data, &matches);
270        assert_eq!(
271            out,
272            format!(
273                "{} path/to/fileA
274{} path/to/fileB
275{} path/to/fileC
276{} path/to/fileD
277{} path/to/fileE
278{} path/to/fileF
279{} path/to/fileG
280{} path/to/fileH
281{} path/to/fileI
282{} path/to/fileJ
283{} path/to/fileK
284{} path/to/fileL
285{} path/to/fileM
286{} path/to/fileN
287{} path/to/fileO
288{} path/to/fileP
289{} path/to/fileQ
290{} path/to/fileR
291{} path/to/fileS
292{} path/to/fileT
293",
294                "    2K".yellow().bold(),
295                "    1K".yellow().bold(),
296                "  1023".yellow().bold(),
297                "  1022".yellow().bold(),
298                "  1021".yellow().bold(),
299                "  1020".yellow().bold(),
300                "  1019".yellow().bold(),
301                "  1018".yellow().bold(),
302                "  1017".yellow().bold(),
303                "  1016".yellow().bold(),
304                "  1015".yellow().bold(),
305                "  1014".yellow().bold(),
306                "  1013".yellow().bold(),
307                "  1012".yellow().bold(),
308                "  1011".yellow().bold(),
309                "  1010".yellow().bold(),
310                "  1009".yellow().bold(),
311                "  1008".yellow().bold(),
312                "  1007".yellow().bold(),
313                "  1006".yellow().bold()
314            )
315            .as_bytes()
316        )
317    }
318
319    #[test]
320    fn simple_units_bytes() {
321        assert_eq!(simple_units(100), "   100");
322    }
323
324    #[test]
325    fn simple_units_kbytes() {
326        assert_eq!(simple_units(1025), "    1K");
327    }
328
329    #[test]
330    fn simple_units_kbytes_long() {
331        assert_eq!(simple_units(1025000), " 1000K");
332    }
333
334    #[test]
335    fn simple_units_mbytes() {
336        assert_eq!(simple_units(2_200_000), "    2M");
337    }
338
339    #[test]
340    fn color_black() {
341        let args = vec!["ds", "-c", "black"];
342        let matches = App::new("DiskSpace")
343            .arg(
344                Arg::with_name("color")
345                    .short("c")
346                    .value_name("COLOR")
347                    .takes_value(true),
348            )
349            .get_matches_from(args);
350        env::set_var("TERM", "xterm-256color");
351
352        let result = color(10, &matches);
353        assert_eq!(result, "    10".black().bold().to_string());
354    }
355
356    #[test]
357    fn color_red() {
358        let args = vec!["ds", "-c", "red"];
359        let matches = App::new("DiskSpace")
360            .arg(
361                Arg::with_name("color")
362                    .short("c")
363                    .value_name("COLOR")
364                    .takes_value(true),
365            )
366            .get_matches_from(args);
367        env::set_var("TERM", "xterm-256color");
368
369        let result = color(10, &matches);
370        assert_eq!(result, "    10".red().bold().to_string());
371    }
372
373    #[test]
374    fn color_green() {
375        let args = vec!["ds", "-c", "green"];
376        let matches = App::new("DiskSpace")
377            .arg(
378                Arg::with_name("color")
379                    .short("c")
380                    .value_name("COLOR")
381                    .takes_value(true),
382            )
383            .get_matches_from(args);
384        env::set_var("TERM", "xterm-256color");
385
386        let result = color(10, &matches);
387        assert_eq!(result, "    10".green().bold().to_string());
388    }
389
390    #[test]
391    fn color_yellow() {
392        let args = vec!["ds", "-c", "yellow"];
393        let matches = App::new("DiskSpace")
394            .arg(
395                Arg::with_name("color")
396                    .short("c")
397                    .value_name("COLOR")
398                    .takes_value(true),
399            )
400            .get_matches_from(args);
401        env::set_var("TERM", "xterm-256color");
402
403        let result = color(10, &matches);
404        assert_eq!(result, "    10".yellow().bold().to_string());
405    }
406
407    #[test]
408    fn color_blue() {
409        let args = vec!["ds", "-c", "blue"];
410        let matches = App::new("DiskSpace")
411            .arg(
412                Arg::with_name("color")
413                    .short("c")
414                    .value_name("COLOR")
415                    .takes_value(true),
416            )
417            .get_matches_from(args);
418        env::set_var("TERM", "xterm-256color");
419
420        let result = color(10, &matches);
421        assert_eq!(result, "    10".blue().bold().to_string());
422    }
423
424    #[test]
425    fn color_magenta() {
426        let args = vec!["ds", "-c", "magenta"];
427        let matches = App::new("DiskSpace")
428            .arg(
429                Arg::with_name("color")
430                    .short("c")
431                    .value_name("COLOR")
432                    .takes_value(true),
433            )
434            .get_matches_from(args);
435        env::set_var("TERM", "xterm-256color");
436
437        let result = color(10, &matches);
438        assert_eq!(result, "    10".magenta().bold().to_string());
439    }
440
441    #[test]
442    fn color_cyan() {
443        let args = vec!["ds", "-c", "cyan"];
444        let matches = App::new("DiskSpace")
445            .arg(
446                Arg::with_name("color")
447                    .short("c")
448                    .value_name("COLOR")
449                    .takes_value(true),
450            )
451            .get_matches_from(args);
452        env::set_var("TERM", "xterm-256color");
453
454        let result = color(10, &matches);
455        assert_eq!(result, "    10".cyan().bold().to_string());
456    }
457
458    #[test]
459    fn color_white() {
460        let args = vec!["ds", "-c", "white"];
461        let matches = App::new("DiskSpace")
462            .arg(
463                Arg::with_name("color")
464                    .short("c")
465                    .value_name("COLOR")
466                    .takes_value(true),
467            )
468            .get_matches_from(args);
469        env::set_var("TERM", "xterm-256color");
470
471        let result = color(10, &matches);
472        assert_eq!(result, "    10".white().bold().to_string());
473    }
474
475    #[test]
476    fn color_none() {
477        let args = vec!["ds", "-c", "none"];
478        let matches = App::new("DiskSpace")
479            .arg(
480                Arg::with_name("color")
481                    .short("c")
482                    .value_name("COLOR")
483                    .takes_value(true),
484            )
485            .get_matches_from(args);
486        env::set_var("TERM", "xterm-256color");
487
488        let result = color(10, &matches);
489        assert_eq!(result, "    10");
490    }
491
492    #[test]
493    fn settings_defaults() {
494        let args = vec!["ds"];
495        let matches = App::new("DiskSpace").get_matches_from(args);
496        let mut rs = ReportSettings::new();
497        rs.settings(&matches);
498        assert_eq!(rs.all, false);
499        assert_eq!(rs.reverse, false);
500        assert_eq!(rs.lines, 20);
501    }
502
503    #[test]
504    fn settings_all() {
505        let args = vec!["ds", "-a"];
506        let matches = App::new("DiskSpace")
507            .arg(Arg::with_name("all").short("a"))
508            .get_matches_from(args);
509        let mut rs = ReportSettings::new();
510        rs.settings(&matches);
511        assert_eq!(rs.all, true);
512    }
513
514    #[test]
515    fn settings_reverse() {
516        let args = vec!["ds", "-r"];
517        let matches = App::new("DiskSpace")
518            .arg(Arg::with_name("reverse").short("r"))
519            .get_matches_from(args);
520        let mut rs = ReportSettings::new();
521        rs.settings(&matches);
522        assert_eq!(rs.reverse, true);
523    }
524
525    #[test]
526    fn settings_lines() {
527        let args = vec!["ds", "-n", "10"];
528        let matches = App::new("DiskSpace")
529            .arg(Arg::with_name("lines").short("n").takes_value(true))
530            .get_matches_from(args);
531        let mut rs = ReportSettings::new();
532        rs.settings(&matches);
533        assert_eq!(rs.lines, 10);
534    }
535
536    #[test]
537    fn settings_lines_invalid_value() {
538        let args = vec!["ds", "-n", "apple"];
539        let matches = App::new("DiskSpace")
540            .arg(Arg::with_name("lines").short("n").takes_value(true))
541            .get_matches_from(args);
542        let mut rs = ReportSettings::new();
543        rs.settings(&matches);
544        assert_eq!(rs.lines, 20);
545    }
546
547    #[test]
548    fn settings_exclude() {
549        let args = vec!["ds", "-e", "apple", "pear"];
550        let matches = App::new("DiskSpace")
551            .arg(
552                Arg::with_name("exclude")
553                    .short("e")
554                    .min_values(1)
555                    .multiple(true)
556                    .takes_value(true),
557            )
558            .get_matches_from(args);
559        let mut rs = ReportSettings::new();
560        rs.settings(&matches);
561        assert_eq!(rs.exclude, vec!["apple".to_string(), "pear".to_string()]);
562    }
563
564}