flexi_logger/parameters/
file_spec.rs

1use crate::writers::file_log_writer::InfixFilter;
2use crate::{DeferredNow, FlexiLoggerError};
3use std::{
4    ffi::{OsStr, OsString},
5    ops::Add,
6    path::{Path, PathBuf},
7};
8/// Builder object for specifying the name and path of the log output file.
9///
10/// The filename is built from several partially components, using this pattern:
11///
12/// ```<filename> = [<basename>][_][<discriminant>][_][<starttime>][_][<infix>][.<suffix>]```
13///
14/// - `[<basename>]`: This is by default the program's name, but can be set to a different value
15///   or suppressed at all.
16///
17/// - `[_]`: Consecutive name parts are separated by an underscore.
18///   No underscore is used at the beginning of the filename and directly before the suffix.
19///
20/// - `[<discriminant>]`: some optional name part that allows further differentiations.
21///
22/// - `[<starttime>]`: denotes the point in time when the program was started, if used.
23///
24/// - `[infix]`: used with rotation to differentiate consecutive files.
25///
26/// Without rotation, the default filename pattern uses the program name as basename,
27/// no discriminant, the timestamp of the program start
28/// (printed in the format "YYYY-MM-DD_hh-mm-ss"),
29/// and the suffix `.log`, e.g.
30///
31/// ```myprog_2015-07-08_10-44-11.log```.
32///
33/// This ensures that with every program start a new trace file is written that can easily
34/// be associated with a concrete program run.
35///
36/// When the timestamp is suppressed with [`FileSpec::suppress_timestamp`],
37/// you get a fixed output file name.
38/// It is then worth considering whether a new program start should discard
39/// the content of an already existing outputfile or if it should append its new content to it
40/// (see [`Logger::append`](crate::Logger::append)).
41///
42/// With rotation, the timestamp is by default suppressed and instead the infix is used.
43/// The infix starts always with "r".
44/// For more details how its precise content can be influenced, see [`Naming`](crate::Naming).
45///
46#[derive(Debug, Clone, Eq, PartialEq)]
47pub struct FileSpec {
48    pub(crate) directory: PathBuf,
49    pub(crate) basename: String,
50    pub(crate) o_discriminant: Option<String>,
51    timestamp_cfg: TimestampCfg,
52    o_suffix: Option<String>,
53    pub(crate) use_utc: bool,
54}
55impl Default for FileSpec {
56    /// Describes a file in the current folder,
57    /// using, as its filestem, the program name followed by the current timestamp,
58    /// and the suffix ".log".
59    #[must_use]
60    fn default() -> Self {
61        FileSpec {
62            directory: PathBuf::from("."),
63            basename: Self::default_basename(),
64            o_discriminant: None,
65            timestamp_cfg: TimestampCfg::Default,
66            o_suffix: Some(String::from("log")),
67            use_utc: false,
68        }
69    }
70}
71impl FileSpec {
72    fn default_basename() -> String {
73        let arg0 = std::env::args().next().unwrap_or_else(|| "rs".to_owned());
74        Path::new(&arg0).file_stem().map(OsStr::to_string_lossy).unwrap(/*cannot fail*/).to_string()
75    }
76
77    /// The provided path should describe a log file.
78    /// If it exists, it must be a file, not a folder.
79    /// If necessary, parent folders will be created.
80    ///
81    /// ```rust
82    /// # use flexi_logger::FileSpec;
83    /// assert_eq!(
84    ///     FileSpec::default()
85    ///         .directory("/a/b/c")
86    ///         .basename("foo")
87    ///         .suppress_timestamp()
88    ///         .suffix("bar"),
89    ///     FileSpec::try_from("/a/b/c/foo.bar").unwrap()
90    /// );
91    /// ```
92    /// # Errors
93    ///
94    /// [`FlexiLoggerError::OutputBadFile`] if the given path exists and is a folder.
95    ///
96    /// # Panics
97    ///
98    /// Panics if the basename of the given path has no filename
99    pub fn try_from<P: Into<PathBuf>>(p: P) -> Result<Self, FlexiLoggerError> {
100        let p: PathBuf = p.into();
101        if p.is_dir() {
102            Err(FlexiLoggerError::OutputBadFile)
103        } else {
104            Ok(FileSpec {
105                directory: p.parent().unwrap(/*cannot fail*/).to_path_buf(),
106                basename: p.file_stem().unwrap(/*ok*/).to_string_lossy().to_string(),
107                o_discriminant: None,
108                o_suffix: p.extension().map(|s| s.to_string_lossy().to_string()),
109                timestamp_cfg: TimestampCfg::No,
110                use_utc: false,
111            })
112        }
113    }
114
115    /// Makes the logger not include a basename into the names of the log files
116    ///
117    /// Equivalent to `basename("")`.
118    #[must_use]
119    pub fn suppress_basename(self) -> Self {
120        self.basename("")
121    }
122
123    /// The specified String is used as the basename of the log file name,
124    /// instead of the program name. Using a file separator within the argument is discouraged.
125    #[must_use]
126    pub fn basename<S: Into<String>>(mut self, basename: S) -> Self {
127        self.basename = basename.into();
128        self
129    }
130
131    /// The specified String is used as the basename of the log file,
132    /// instead of the program name, which is used when `None` is given.
133    #[must_use]
134    pub fn o_basename<S: Into<String>>(mut self, o_basename: Option<S>) -> Self {
135        self.basename = o_basename.map_or_else(Self::default_basename, Into::into);
136        self
137    }
138
139    /// Specifies a folder for the log files.
140    ///
141    /// If the specified folder does not exist, it will be created.
142    /// By default, the log files are created in the folder where the program was started.
143    #[must_use]
144    pub fn directory<P: Into<PathBuf>>(mut self, directory: P) -> Self {
145        self.directory = directory.into();
146        self
147    }
148
149    /// Specifies a folder for the log files.
150    ///
151    /// If the specified folder does not exist, it will be created.
152    /// With None, the log files are created in the folder where the program was started.
153    #[must_use]
154    pub fn o_directory<P: Into<PathBuf>>(mut self, directory: Option<P>) -> Self {
155        self.directory = directory.map_or_else(|| PathBuf::from("."), Into::into);
156        self
157    }
158
159    /// The specified String is added to the log file name.
160    #[must_use]
161    pub fn discriminant<S: Into<String>>(self, discriminant: S) -> Self {
162        self.o_discriminant(Some(discriminant))
163    }
164
165    /// The specified String is added to the log file name.
166    #[must_use]
167    pub fn o_discriminant<S: Into<String>>(mut self, o_discriminant: Option<S>) -> Self {
168        self.o_discriminant = o_discriminant.map(Into::into);
169        self
170    }
171    /// Specifies a suffix for the log files.
172    ///
173    /// Equivalent to `o_suffix(Some(suffix))`.
174    #[must_use]
175    pub fn suffix<S: Into<String>>(self, suffix: S) -> Self {
176        self.o_suffix(Some(suffix))
177    }
178
179    /// Specifies a suffix for the log files, or supresses the use of a suffix completely.
180    ///
181    /// The default suffix is "log".
182    #[must_use]
183    pub fn o_suffix<S: Into<String>>(mut self, o_suffix: Option<S>) -> Self {
184        self.o_suffix = o_suffix.map(Into::into);
185        self
186    }
187
188    /// Makes the logger not include the start time into the names of the log files
189    ///
190    /// Equivalent to `use_timestamp(false)`.
191    #[must_use]
192    pub fn suppress_timestamp(self) -> Self {
193        self.use_timestamp(false)
194    }
195
196    /// Defines if the start time should be included into the names of the log files.
197    ///
198    /// The _default_ behavior depends on the usage:
199    /// - without rotation, a timestamp is by default included into the name
200    /// - with rotation, the timestamp is by default suppressed
201    #[must_use]
202    pub fn use_timestamp(mut self, use_timestamp: bool) -> Self {
203        self.timestamp_cfg = if use_timestamp {
204            TimestampCfg::Yes
205        } else {
206            TimestampCfg::No
207        };
208        self
209    }
210
211    #[doc(hidden)]
212    #[must_use]
213    pub fn used_directory(&self) -> PathBuf {
214        self.directory.clone()
215    }
216    pub(crate) fn has_basename(&self) -> bool {
217        !self.basename.is_empty()
218    }
219    pub(crate) fn has_discriminant(&self) -> bool {
220        self.o_discriminant.is_some()
221    }
222    pub(crate) fn uses_timestamp(&self) -> bool {
223        matches!(self.timestamp_cfg, TimestampCfg::Yes)
224    }
225
226    // If no decision was done yet, decide now whether to include a timestamp
227    // into the names of the log files.
228    pub(crate) fn if_default_use_timestamp(&mut self, use_timestamp: bool) {
229        if let TimestampCfg::Default = self.timestamp_cfg {
230            self.timestamp_cfg = if use_timestamp {
231                TimestampCfg::Yes
232            } else {
233                TimestampCfg::No
234            };
235        }
236    }
237
238    pub(crate) fn get_directory(&self) -> PathBuf {
239        self.directory.clone()
240    }
241
242    pub(crate) fn get_suffix(&self) -> Option<String> {
243        self.o_suffix.clone()
244    }
245
246    // basename + o_discriminant + o_timestamp
247    pub(crate) fn fixed_name_part(&self) -> String {
248        let mut fixed_name_part = self.basename.clone();
249        fixed_name_part.reserve(50);
250
251        if let Some(discriminant) = &self.o_discriminant {
252            append_underscore_if_not_empty(&mut fixed_name_part);
253            fixed_name_part.push_str(discriminant);
254        }
255        if let Some(timestamp) = &self.timestamp_cfg.get_timestamp() {
256            append_underscore_if_not_empty(&mut fixed_name_part);
257            fixed_name_part.push_str(timestamp);
258        }
259        fixed_name_part
260    }
261
262    /// Derives a `PathBuf` from the spec and the given infix.
263    #[must_use]
264    pub fn as_pathbuf(&self, o_infix: Option<&str>) -> PathBuf {
265        let mut filename = self.fixed_name_part();
266
267        if let Some(infix) = o_infix {
268            if !infix.is_empty() {
269                append_underscore_if_not_empty(&mut filename);
270                filename.push_str(infix);
271            }
272        };
273        if let Some(suffix) = &self.o_suffix {
274            filename.push('.');
275            filename.push_str(suffix);
276        }
277
278        let mut p_path = self.directory.clone();
279        p_path.push(filename);
280        p_path
281    }
282
283    // handles collisions by appending ".restart-<number>" to the infix, if necessary
284    pub(crate) fn collision_free_infix_for_rotated_file(&self, infix: &str) -> String {
285        let uncompressed_files = self.list_of_files(
286            &InfixFilter::Equls(infix.to_string()),
287            self.o_suffix.as_deref(),
288        );
289        let compressed_files =
290            self.list_of_files(&InfixFilter::Equls(infix.to_string()), Some("gz"));
291
292        let mut restart_siblings = uncompressed_files
293            .into_iter()
294            .chain(compressed_files)
295            .filter(|pb| {
296                // ignore .gz suffix
297                let mut pb2 = PathBuf::from(pb);
298                if pb2.extension() == Some(OsString::from("gz").as_ref()) {
299                    pb2.set_extension("");
300                };
301                // suffix must match the given suffix, if one is given
302                match self.o_suffix {
303                    Some(ref sfx) => pb2.extension() == Some(OsString::from(sfx).as_ref()),
304                    None => true,
305                }
306            })
307            .filter(|pb| {
308                pb.file_name()
309                    .unwrap()
310                    .to_string_lossy()
311                    .contains(".restart-")
312            })
313            .collect::<Vec<PathBuf>>();
314
315        let new_path = self.as_pathbuf(Some(infix));
316        let new_path_with_gz = {
317            let mut new_path_with_gz = new_path.clone();
318            new_path_with_gz
319                .set_extension([self.o_suffix.as_deref().unwrap_or(""), ".gz"].concat());
320            new_path_with_gz
321        };
322
323        // if collision would occur (new_path or compressed new_path exists already),
324        // find highest restart and add 1, else continue without restart
325        if new_path.exists() || new_path_with_gz.exists() || !restart_siblings.is_empty() {
326            let next_number = if restart_siblings.is_empty() {
327                0
328            } else {
329                restart_siblings.sort_unstable();
330                let new_path = restart_siblings.pop().unwrap(/*ok*/);
331                let file_stem_string = if self.o_suffix.is_some() {
332                    new_path
333                    .file_stem().unwrap(/*ok*/)
334                    .to_string_lossy().to_string()
335                } else {
336                    new_path.to_string_lossy().to_string()
337                };
338                let index = file_stem_string.find(".restart-").unwrap(/*ok*/);
339                file_stem_string[(index + 9)..(index + 13)].parse::<usize>().unwrap(/*ok*/) + 1
340            };
341
342            infix.to_string().add(&format!(".restart-{next_number:04}"))
343        } else {
344            infix.to_string()
345        }
346    }
347
348    pub(crate) fn list_of_files(
349        &self,
350        infix_filter: &InfixFilter,
351        o_suffix: Option<&str>,
352    ) -> Vec<PathBuf> {
353        self.filter_files(&self.read_dir_related_files(), infix_filter, o_suffix)
354    }
355
356    // returns an ordered list of all files in the right directory that start with the fixed_name_part
357    pub(crate) fn read_dir_related_files(&self) -> Vec<PathBuf> {
358        let fixed_name_part = self.fixed_name_part();
359        let mut log_files = std::fs::read_dir(&self.directory)
360            .unwrap(/*ignore errors from reading the directory*/)
361            .flatten(/*ignore errors from reading entries in the directory*/)
362            .filter(|entry| entry.path().is_file())
363            .map(|de| de.path())
364            .filter(|path| {
365                // fixed name part must match
366                if let Some(fln) = path.file_name() {
367                    fln.to_string_lossy(/*good enough*/).starts_with(&fixed_name_part)
368                } else {
369                    false
370                }
371            })
372            .collect::<Vec<PathBuf>>();
373        log_files.sort_unstable();
374        log_files.reverse();
375        log_files
376    }
377
378    pub(crate) fn filter_files(
379        &self,
380        files: &[PathBuf],
381        infix_filter: &InfixFilter,
382        o_suffix: Option<&str>,
383    ) -> Vec<PathBuf> {
384        let fixed_name_part = self.fixed_name_part();
385        files
386            .iter()
387            .filter(|path| {
388                // if suffix is specified, it must match
389                if let Some(suffix) = o_suffix {
390                    path.extension().is_some_and(|ext| {
391                        let s = ext.to_string_lossy();
392                        s == suffix
393                    })
394                } else {
395                    true
396                }
397            })
398            .filter(|path| {
399                // infix filter must pass
400                let stem = path.file_stem().unwrap(/* CANNOT FAIL*/).to_string_lossy();
401                let infix_start = if fixed_name_part.is_empty() {
402                    0
403                } else {
404                    fixed_name_part.len() + 1 // underscore at the end
405                };
406                if stem.len() <= infix_start {
407                    return false;
408                }
409                let maybe_infix = &stem[infix_start..];
410                let end = maybe_infix.find('.').unwrap_or(maybe_infix.len());
411                infix_filter.filter_infix(&maybe_infix[..end])
412            })
413            .map(PathBuf::clone)
414            .collect::<Vec<PathBuf>>()
415    }
416
417    #[cfg(test)]
418    pub(crate) fn get_timestamp(&self) -> Option<String> {
419        self.timestamp_cfg.get_timestamp()
420    }
421}
422
423fn append_underscore_if_not_empty(filename: &mut String) {
424    if !filename.is_empty() {
425        filename.push('_');
426    }
427}
428
429const TS_USCORE_DASHES_USCORE_DASHES: &str = "%Y-%m-%d_%H-%M-%S";
430
431#[derive(Debug, Clone, Eq, PartialEq)]
432enum TimestampCfg {
433    Default,
434    Yes,
435    No,
436}
437impl TimestampCfg {
438    fn get_timestamp(&self) -> Option<String> {
439        match self {
440            Self::Default | Self::Yes => Some(
441                DeferredNow::new()
442                    .format(TS_USCORE_DASHES_USCORE_DASHES)
443                    .to_string(),
444            ),
445            Self::No => None,
446        }
447    }
448}
449
450#[cfg(test)]
451mod test {
452    use super::{FileSpec, TimestampCfg};
453    use crate::writers::file_log_writer::InfixFilter;
454    use std::{
455        fs::File,
456        path::{Path, PathBuf},
457    };
458
459    #[test]
460    fn test_timstamp_cfg() {
461        let ts = TimestampCfg::Yes;
462        let s = ts.get_timestamp().unwrap(/* OK */);
463        let bytes = s.into_bytes();
464        assert_eq!(bytes[4], b'-');
465        assert_eq!(bytes[7], b'-');
466        assert_eq!(bytes[10], b'_');
467        assert_eq!(bytes[13], b'-');
468        assert_eq!(bytes[16], b'-');
469    }
470
471    #[test]
472    fn test_default() {
473        let path = FileSpec::default().as_pathbuf(None);
474        assert_file_spec(&path, &PathBuf::from("."), true, "log");
475    }
476
477    // todo: does not support suppress_timestamp & suppress_basename & use discriminant
478    fn assert_file_spec(path: &Path, folder: &Path, with_timestamp: bool, suffix: &str) {
479        // check folder
480        assert_eq!(
481            path.parent().unwrap(), // .canonicalize().unwrap()
482            folder                  // .canonicalize().unwrap()
483        );
484        // check file stem
485        //  - should start with progname
486        let progname = PathBuf::from(std::env::args().next().unwrap())
487            .file_stem()
488            .unwrap()
489            .to_string_lossy()
490            .clone()
491            .to_string();
492        let stem = path
493            .file_stem()
494            .unwrap()
495            .to_string_lossy()
496            .clone()
497            .to_string();
498        assert!(
499            stem.starts_with(&progname),
500            "stem: {stem:?}, progname: {progname:?}",
501        );
502        if with_timestamp {
503            // followed by _ and timestamp
504            assert_eq!(stem.as_bytes()[progname.len()], b'_');
505            let s_ts = &stem[progname.len() + 1..];
506            assert!(
507                chrono::NaiveDateTime::parse_from_str(s_ts, "%Y-%m-%d_%H-%M-%S").is_ok(),
508                "s_ts: \"{s_ts}\"",
509            );
510        } else {
511            assert_eq!(
512                stem.len(),
513                progname.len(),
514                "stem: {stem:?}, progname: {progname:?}",
515            );
516        }
517
518        // check suffix
519        assert_eq!(path.extension().unwrap(), suffix);
520    }
521
522    #[test]
523    fn test_if_default_use_timestamp() {
524        // default() + if_default_use_timestamp(false) => false
525        {
526            let mut fs = FileSpec::default();
527            fs.if_default_use_timestamp(false);
528            let path = fs.as_pathbuf(None);
529            assert_file_spec(&path, &PathBuf::from("."), false, "log");
530        }
531        // default() + use_timestamp(true) + if_default_use_timestamp(false) => true
532        {
533            let mut fs = FileSpec::default().use_timestamp(true);
534            fs.if_default_use_timestamp(false);
535            let path = fs.as_pathbuf(None);
536            assert_file_spec(&path, &PathBuf::from("."), true, "log");
537        }
538        // default() + use_timestamp(false) + if_default_use_timestamp(true) +  => true
539        {
540            let mut fs = FileSpec::default();
541            fs.if_default_use_timestamp(false);
542            let path = fs.use_timestamp(true).as_pathbuf(None);
543            assert_file_spec(&path, &PathBuf::from("."), true, "log");
544        }
545        // default() + if_default_use_timestamp(false) + use_timestamp(true) => true
546        {
547            let mut fs = FileSpec::default();
548            fs.if_default_use_timestamp(false);
549            let path = fs.use_timestamp(true).as_pathbuf(None);
550            assert_file_spec(&path, &PathBuf::from("."), true, "log");
551        }
552    }
553
554    #[test]
555    fn test_from_url() {
556        let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
557            .unwrap()
558            .as_pathbuf(None);
559        // check folder
560        assert_eq!(path.parent().unwrap(), PathBuf::from("/a/b/c"));
561        // check filestem
562        //  - should start with progname
563        let stem = path
564            .file_stem()
565            .unwrap()
566            .to_string_lossy()
567            .clone()
568            .to_string();
569        assert_eq!(stem, "d_foo_bar");
570
571        // check suffix
572        assert_eq!(path.extension().unwrap(), "trc");
573    }
574
575    #[test]
576    fn test_basename() {
577        {
578            let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
579                .unwrap()
580                .o_basename(Some("boo_far"))
581                .as_pathbuf(None);
582            // check folder
583            assert_eq!(path.parent().unwrap(), PathBuf::from("/a/b/c"));
584
585            // check filestem
586            //  - should start with progname
587            let stem = path
588                .file_stem()
589                .unwrap()
590                .to_string_lossy()
591                .clone()
592                .to_string();
593            assert_eq!(stem, "boo_far");
594
595            // check suffix
596            assert_eq!(path.extension().unwrap(), "trc");
597        }
598        {
599            let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
600                .unwrap()
601                .o_basename(Option::<String>::None)
602                .as_pathbuf(None);
603            assert_file_spec(&path, &PathBuf::from("/a/b/c"), false, "trc");
604        }
605    }
606
607    #[test]
608    fn test_directory_and_suffix() {
609        {
610            let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
611                .unwrap()
612                .directory("/x/y/z")
613                .o_suffix(Some("txt"))
614                .o_basename(Option::<String>::None)
615                .as_pathbuf(None);
616            assert_file_spec(&path, &PathBuf::from("/x/y/z"), false, "txt");
617        }
618    }
619
620    #[test]
621    fn test_discriminant() {
622        let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
623            .unwrap()
624            .directory("/x/y/z")
625            .o_suffix(Some("txt"))
626            .o_discriminant(Some("1234"))
627            .as_pathbuf(None);
628        assert_eq!(
629            path.file_name().unwrap().to_str().unwrap(),
630            "d_foo_bar_1234.txt"
631        );
632    }
633
634    #[test]
635    fn test_suppress_basename() {
636        let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
637            .unwrap()
638            .suppress_basename()
639            .o_suffix(Some("txt"))
640            .o_discriminant(Some("1234"))
641            .as_pathbuf(None);
642        assert_eq!(path.file_name().unwrap().to_str().unwrap(), "1234.txt");
643    }
644
645    #[test]
646    fn test_empty_base_name() {
647        let path = FileSpec::default()
648            .suppress_basename()
649            .suppress_timestamp()
650            .o_discriminant(Option::<String>::None)
651            .as_pathbuf(None);
652        assert_eq!(path.file_name().unwrap(), ".log");
653    }
654
655    #[test]
656    fn test_empty_name() {
657        let path = FileSpec::default()
658            .suppress_basename()
659            .suppress_timestamp()
660            .o_suffix(Option::<String>::None)
661            .as_pathbuf(None);
662        assert!(path.file_name().is_none());
663    }
664
665    #[test]
666    fn issue_178() {
667        let path = FileSpec::default()
668            .basename("BASENAME")
669            .suppress_timestamp()
670            .as_pathbuf(Some(""));
671        assert_eq!(path.file_name().unwrap().to_string_lossy(), "BASENAME.log");
672
673        let path = FileSpec::default()
674            .basename("BASENAME")
675            .discriminant("1")
676            .suppress_timestamp()
677            .as_pathbuf(Some(""));
678        assert_eq!(
679            path.file_name().unwrap().to_string_lossy(),
680            "BASENAME_1.log"
681        );
682    }
683
684    #[test]
685    fn test_list_of_files() {
686        let dir = temp_dir::TempDir::new().unwrap();
687        let pd = dir.path();
688        let filespec: FileSpec = FileSpec::default()
689            .directory(pd)
690            .basename("Base")
691            .discriminant("Discr")
692            .use_timestamp(true);
693        println!("Filespec: {}", filespec.as_pathbuf(Some("Infix")).display());
694
695        let mut fn1 = String::new();
696        fn1.push_str("Base_Discr_");
697        fn1.push_str(&filespec.get_timestamp().unwrap());
698        fn1.push_str("_Infix");
699        fn1.push_str(".log");
700        assert_eq!(
701            filespec
702                .as_pathbuf(Some("Infix"))
703                .file_name()
704                .unwrap()
705                .to_string_lossy(),
706            fn1
707        );
708        // create typical set of files, and noise
709        create_file(pd, "test1.txt");
710        create_file(pd, &build_filename(&filespec, "Infix1"));
711        create_file(pd, &build_filename(&filespec, "Infix2"));
712
713        println!("\nFolder content:");
714        for entry in std::fs::read_dir(pd).unwrap() {
715            println!("  {}", entry.unwrap().path().display());
716        }
717
718        println!("\nRelevant subset:");
719        for pb in filespec.list_of_files(&InfixFilter::StartsWth("Infix".to_string()), Some("log"))
720        {
721            println!("  {}", pb.display());
722        }
723    }
724
725    fn build_filename(file_spec: &FileSpec, infix: &str) -> String {
726        let mut fn1 = String::new();
727        fn1.push_str("Base_Discr_");
728        fn1.push_str(&file_spec.get_timestamp().unwrap());
729        fn1.push('_');
730        fn1.push_str(infix);
731        fn1.push_str(".log");
732        fn1
733    }
734
735    fn create_file(dir: &Path, filename: &str) {
736        File::create(dir.join(filename)).unwrap();
737    }
738}