cotton/
lib.rs

1/*!
2"Batteries included" prelude with crates, types and functions useful for writing command-line interface tools and quick scripts.
3
4This prelude aims to be useful in generic context of CLI tools and will try to minimise dependencies.
5
6# Basic command-line interface program template
7
8Example starting point for your program that features command line argument parsing with help message, logger setup and
9human-friendly error and panic messages.
10
11```
12use cotton::prelude::*;
13
14/// Example script description
15#[derive(Parser)]
16struct Cli {
17    #[command(flatten)]
18    logging: ArgsLogger,
19
20    #[command(flatten)]
21    dry_run: ArgsDryRun,
22}
23
24fn main() -> FinalResult {
25    let Cli {
26        logging,
27        dry_run,
28    } = Cli::parse();
29    setup_logger(logging, vec![module_path!()]);
30
31    if !dry_run.enabled {
32        warn!("Hello world!");
33    }
34
35    Ok(())
36}
37```
38
39# Features
40
41A small list of crates is always included in cotton. These are adding some common data types, language usability aids and common
42standard library imports:
43
44* [itertools](https://docs.rs/itertools) - extends standard iterators
45* [linked-hash-map](https://docs.rs/linked-hash-map) and [linked-hash_set](https://docs.rs/linked-hash_set) - ordered maps and sets
46* [maybe-string](https://docs.rs/maybe-string) - handle probably UTF-8 encoded binary data
47* [boolinator](https://docs.rs/boolinator) - convert [Option] to [bool]
48* [tap](https://docs.rs/tap) - avoid need for `let` bindings
49
50Cotton will also always import large number of commonly used standard library items.
51
52All other dependencies are optional and can be opted-out by disabling default features and opting-in to only selected crates.
53
54For convenience there are features defined that group several crates together:
55
56* `regex` - regular expressions
57  * [regex](https://docs.rs/regex) - An implementation of regular expressions for Rust
58* `args` - parsing of command line arguments
59  * [clap](https://docs.rs/clap) - A simple to use, efficient, and full-featured Command Line Argument Parser
60* `logging` - logging macros and logger
61  * [log](https://docs.rs/log) - A lightweight logging facade for Rust
62  * [stderrlog](https://docs.rs/stderrlog) - Logger that logs to stderr based on verbosity specified
63* `time` - time and date
64  * [chrono](https://docs.rs/chrono) - Date and time library for Rust
65* `term` - working with terminal emulators
66  * [ansi_term](https://docs.rs/ansi_term) - Library for ANSI terminal colours and styles (bold, underline)
67  * [atty](https://docs.rs/atty) - A simple interface for querying atty
68  * [zzz](https://docs.rs/zzz) - Fast progress bar with sane defaults
69  * [term_size](https://docs.rs/term_size) - functions for determining terminal sizes and dimensions
70* `hashing` - digest calculations and hex encoding
71  * [hex](https://docs.rs/hex) - Encoding and decoding data into/from hexadecimal representation
72  * [sha2](https://docs.rs/sha2) - Pure Rust implementation of the SHA-2 hash function family
73  * [digest](https://docs.rs/digest) - Traits for cryptographic hash functions and message authentication codes
74* `files` - file metadata and temporary files
75  * [tempfile](https://docs.rs/tempfile) - A library for managing temporary files and directories
76  * [filetime](https://docs.rs/filetime) - Platform-agnostic accessors of timestamps in File metadata
77  * [file-mode](https://docs.rs/file-mode) - Decode Unix file mode bits, change them and apply them to files
78  * [file-owner](https://docs.rs/file-owner) - Set and get Unix file owner and group
79* `signals` - UNIX signal handling
80  * [signal-hook](https://docs.rs/signal-hook) - Unix signal handling
81  * [uninterruptible](https://docs.rs/uninterruptible) - Guard type that keeps selected Unix signals suppressed
82* `errors` - flexible error handling and error context
83  * [problem](https://docs.rs/problem) - Error handling for command line applications or prototypes
84  * [error-context](https://docs.rs/error-context) - Methods and types that help with adding additional context information to error types
85  * [scopeguard](https://docs.rs/scopeguard) - A RAII scope guard that will run a given closure when it goes out of scope
86  * [assert_matches](https://docs.rs/assert_matches) - Asserts that a value matches a pattern
87* `app` - application environment
88  * [directories](https://docs.rs/directories) - A tiny mid-level library that provides platform-specific standard locations of directories
89* `process` - running programs and handling input/output
90  * [shellwords](https://docs.rs/shellwords) - Manipulate strings according to the word parsing rules of the UNIX Bourne shell
91  * [exec](https://docs.rs/exec) - Use the POSIX exec function to replace the running program with another
92  * [mkargs](https://docs.rs/mkargs) - Build command arguments
93  * [cradle](https://docs.rs/cradle) - Execute child processes with ease
94
95Non-default features:
96
97* `backtrace` - enable backtraces for [problem::Problem] errors (also run your program with `RUST_BACKTRACE=1`)
98
99For example you my include `cotton` like this in `Cargo.toml`:
100
101```toml
102cotton = { version = "0.1.0", default-features = false, features = ["errors", "args", "logging", "app", "hashing", "process"] }
103```
104
105# Error context
106
107Generally libraries should not add context to the errors as it may be considered sensitive for
108some uses.
109In this library context (like file paths) will be provided by default.
110
111# Static error types
112
113When you need proper error handling (e.g. on the internal modules or when you need to act on
114the errors specifically) use standard way of doing this.
115
116Use enums with `Debug`, `Display` and `Error` trait implementations.
117Add additional `From` implementations to make `?` operator to work.
118
119If you need to add context to an error you can use [error_context](https://docs.rs/error-context) crate that is included in the prelude.
120
121## Example custom static error type implementation
122
123```rust
124use cotton::prelude::*;
125
126#[derive(Debug)]
127enum FileResourceError {
128     FileDigestError(PathBuf, FileDigestError),
129     NotAFileError(PathBuf),
130}
131
132impl Display for FileResourceError {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134     match self {
135         // Do not include chained error message in the message; let the client handle this (e.g. with Problem type)
136         FileResourceError::FileDigestError(path, _) => write!(f, "digest of a file {:?} could not be calculated", path),
137         FileResourceError::NotAFileError(path) => write!(f, "path {:?} is not a file", path),
138     }
139 }
140}
141
142impl Error for FileResourceError {
143 fn source(&self) -> Option<&(dyn Error + 'static)> {
144     match self {
145         // Chain the internal error
146         FileResourceError::FileDigestError(_, err) => Some(err),
147         FileResourceError::NotAFileError(_) => None,
148     }
149 }
150}
151
152// This allows for calls like `foo().wrap_error_while_with(|| self.path.clone())?` to add extra `PathBuf` context to the error
153impl From<ErrorContext<FileDigestError, PathBuf>> for FileResourceError {
154 fn from(err: ErrorContext<FileDigestError, PathBuf>) -> FileResourceError {
155     FileResourceError::FileDigestError(err.context, err.error)
156 }
157}
158```
159*/
160
161#[cfg(feature = "directories")]
162mod app_dir;
163#[cfg(all(feature = "hex", feature = "digest", feature = "sha2"))]
164mod hashing;
165#[cfg(feature = "chrono")]
166mod time;
167mod process;
168
169// All used crates available for direct usage
170
171// Extensions
172pub use itertools;
173pub use linked_hash_map;
174pub use linked_hash_set;
175pub use boolinator;
176pub use tap;
177
178#[cfg(feature = "regex")]
179pub use regex;
180
181// File
182#[cfg(feature = "tempfile")]
183pub use tempfile;
184#[cfg(feature = "filetime")]
185pub use filetime;
186#[cfg(all(target_family = "unix", feature = "file-owner"))]
187pub use file_owner;
188#[cfg(feature = "file-mode")]
189pub use file_mode;
190
191// Error handling
192#[cfg(feature = "problem")]
193pub use problem;
194#[cfg(feature = "error-context")]
195pub use error_context;
196#[cfg(feature = "scopeguard")]
197pub use scopeguard;
198#[cfg(feature = "assert_matches")]
199pub use assert_matches;
200
201// Time/Date
202#[cfg(feature = "chrono")]
203pub use chrono;
204
205// Terminal
206#[cfg(feature = "ansi_term")]
207pub use ansi_term;
208#[cfg(feature = "atty")]
209pub use atty;
210#[cfg(feature = "zzz")]
211pub use zzz;
212#[cfg(feature = "term_size")]
213pub use term_size;
214
215// Argparse
216#[cfg(feature = "clap")]
217pub use clap;
218
219// Logging
220#[cfg(feature = "log")]
221pub use log;
222#[cfg(feature = "stderrlog")]
223pub use stderrlog;
224
225// Hashing
226#[cfg(feature = "sha2")]
227pub use sha2;
228#[cfg(feature = "digest")]
229pub use digest;
230
231// Shellout/processes
232#[cfg(feature = "shellwords")]
233pub use shellwords;
234#[cfg(all(target_family = "unix", feature = "exec"))]
235pub use exec;
236#[cfg(feature = "mkargs")]
237pub use mkargs;
238#[cfg(feature = "cradle")]
239pub use cradle;
240
241// Strings
242#[cfg(feature = "hex")]
243pub use hex;
244pub use maybe_string;
245
246// UNIX signals
247#[cfg(all(target_family = "unix", feature = "signal-hook"))]
248pub use signal_hook;
249#[cfg(all(target_family = "unix", feature = "uninterruptible"))]
250pub use uninterruptible;
251
252// Application environment
253#[cfg(feature = "directories")]
254pub use directories;
255
256pub mod prelude {
257    // Often used I/O
258    pub use std::fs::{
259        canonicalize, copy, create_dir, create_dir_all, hard_link, metadata, read, read_dir,
260        read_link, read_to_string, remove_dir, remove_dir_all, remove_file, rename,
261        set_permissions, symlink_metadata, write, DirBuilder, DirEntry, File, Metadata,
262        OpenOptions, Permissions, ReadDir
263    };
264    pub use std::io::{
265        self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Write, Cursor,
266        Seek, SeekFrom
267    };
268    pub use std::process::{Command, ExitStatus};
269    pub use std::path::{Path, PathBuf};
270    pub use std::ffi::{OsStr, OsString};
271
272    // filesystem
273    #[cfg(feature = "file-mode")]
274    pub use file_mode::{ModeParseError, Mode as FileMode, User, FileType, ProtectionBit, Protection, SpecialBit, Special, set_umask};
275    #[cfg(all(target_family = "unix", feature = "file-mode"))]
276    pub use file_mode::{ModeError, ModePath, ModeFile, SetMode};
277    #[cfg(all(target_family = "unix", feature = "file-owner"))]
278    pub use file_owner::{FileOwnerError, PathExt, group, owner, owner_group, set_group, set_owner, set_owner_group, Group as FileGroup, Owner as FileOwner};
279
280    // Extra traits and stuff
281    pub use std::hash::Hash;
282    pub use std::marker::PhantomData;
283
284    // Patter matching
285    #[cfg(feature = "regex")]
286    pub use regex::{Regex, RegexSet};
287
288    // Temporary files
289    #[cfg(feature = "tempfile")]
290    pub use tempfile::{tempdir, tempfile, spooled_tempfile, tempdir_in, tempfile_in};
291
292    // Timestamps for files
293    #[cfg(feature = "filetime")]
294    pub use filetime::{set_file_atime, set_file_handle_times, set_file_mtime, set_file_times,
295        set_symlink_file_times, FileTime};
296
297    // Often used data structures
298    pub use std::borrow::Cow;
299    pub use std::collections::HashMap;
300    pub use std::collections::HashSet;
301
302    // String helpers
303    pub use maybe_string::{MaybeString, MaybeStr};
304
305    // Ordered HashMap/Set
306    pub use linked_hash_map::LinkedHashMap;
307    pub use linked_hash_set::LinkedHashSet;
308
309    // New std traits
310    pub use std::convert::Infallible;
311    pub use std::convert::TryFrom;
312    pub use std::convert::TryInto; // As we wait for "!"
313
314    // Formatting
315    pub use std::fmt::Write as FmtWrite; // allow write! to &mut String
316    pub use std::fmt::{self, Display, Debug};
317
318    // Arguments
319    #[cfg(feature = "clap")]
320    pub use clap::{self /* needed for derive to work */, Parser, Args, ValueEnum, Subcommand};
321
322    // Error handling
323    pub use std::error::Error;
324    #[cfg(feature = "assert_matches")]
325    pub use assert_matches::assert_matches;
326    #[cfg(feature = "problem")]
327    pub use ::problem::prelude::{problem, in_context_of, in_context_of_with, FailedTo, FailedToIter, Fatal, FatalProblem,
328        MapProblem, MapProblemOr, OkOrProblem, Problem, ProblemWhile, OkOrLog, OkOrLogIter};
329    #[cfg(feature = "problem")]
330    pub use ::problem::result::{FinalResult, Result as PResult};
331    #[cfg(feature = "error-context")]
332    pub use ::error_context::{
333        in_context_of as in_error_context_of, in_context_of_with as in_error_context_of_with, wrap_in_context_of,
334        wrap_in_context_of_with, ErrorContext, ErrorNoContext, MapErrorNoContext, ResultErrorWhile,
335        ResultErrorWhileWrap, ToErrorNoContext, WithContext, WrapContext};
336    #[cfg(feature = "scopeguard")]
337    pub use scopeguard::{defer, defer_on_success, defer_on_unwind, guard, guard_on_success, guard_on_unwind};
338
339    // Running commands
340    #[cfg(feature = "shellwords")]
341    pub use shellwords::{escape as shell_escape, join as shell_join, split as shell_split};
342    pub use crate::process::*;
343    #[cfg(feature = "mkargs")]
344    pub use mkargs::{mkargs, MkArgs};
345    #[cfg(feature = "cradle")]
346    pub use cradle::prelude::*;
347
348    // Content hashing and crypto
349    #[cfg(all(feature = "hex", feature = "digest", feature = "sha2"))]
350    pub use super::hashing::*;
351
352    #[cfg(feature = "hex")]
353    pub use hex::{encode as hex_encode, decode as hex_decode, FromHexError};
354    #[cfg(feature = "sha2")]
355    pub use sha2::digest::{self, generic_array::{self, GenericArray}};
356
357    // Application environment
358    #[cfg(feature = "directories")]
359    pub use super::app_dir::*;
360
361    // Time and duration
362    #[cfg(feature = "chrono")]
363    pub use super::time::*;
364
365    // Iterators
366    pub use itertools::*;
367    pub use std::iter::FromIterator;
368    pub use std::iter::{empty, from_fn, once, once_with, repeat, repeat_with, successors};
369
370    // Signals
371    #[cfg(all(target_family = "unix", feature = "uninterruptible"))]
372    pub use uninterruptible::Uninterruptible;
373    #[cfg(all(target_family = "unix", feature = "signal-hook"))]
374    pub use signal_hook::{consts::signal::*, consts::TERM_SIGNALS, iterator::Signals, flag as signal_flag};
375
376    // Handy extensions
377    pub use boolinator::Boolinator;
378    pub use tap::prelude::{Conv, Tap, Pipe, TapFallible, TapOptional, TryConv};
379
380    // Terminal
381    #[cfg(feature = "ansi_term")]
382    pub use ansi_term::{Colour, Style, ANSIString, ANSIStrings, unstyle};
383    #[cfg(feature = "zzz")]
384    pub use zzz::ProgressBarIterExt;
385    #[cfg(feature = "term_size")]
386    pub use term_size::dimensions as term_dimensions;
387
388    /// Returns true if stdout is a TTY
389    #[cfg(feature = "atty")]
390    pub fn stdout_is_tty() -> bool {
391        atty::is(atty::Stream::Stdout)
392    }
393
394    /// Returns true if stderr is a TTY
395    #[cfg(feature = "atty")]
396    pub fn stderr_is_tty() -> bool {
397        atty::is(atty::Stream::Stdout)
398    }
399
400    // Logging
401    #[cfg(feature = "log")]
402    pub use log::{debug, error, info, log_enabled, trace, warn};
403
404    #[cfg(feature = "clap")]
405    #[derive(Debug, Args)]
406    pub struct ArgsDryRun {
407        /// Just print what would have been done
408        #[arg(long = "dry-run", short = 'd')]
409        pub enabled: bool,
410    }
411
412    #[cfg(all(feature = "clap", feature = "log"))]
413    impl ArgsDryRun {
414        pub fn run(&self, msg: impl Display, run: impl FnOnce() -> ()) -> () {
415            if self.enabled {
416                info!("[dry run]: {}", msg);
417            } else {
418                info!("{}", msg);
419                run()
420            }
421        }
422    }
423
424    #[derive(Debug)]
425    pub enum FileIoError {
426        IoError(PathBuf, io::Error),
427        Utf8Error(PathBuf, std::str::Utf8Error),
428    }
429
430    impl Display for FileIoError {
431        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432            match self {
433                FileIoError::IoError(path, _) => write!(f, "I/O error while reading file {:?}", path),
434                FileIoError::Utf8Error(path, _) => write!(f, "failed to decode content of file {:?} as UTF-8 encoded string", path),
435            }
436        }
437    }
438
439    impl Error for FileIoError {
440        fn source(&self) -> Option<&(dyn Error + 'static)> {
441            match self {
442                FileIoError::IoError(_, err) => Some(err),
443                FileIoError::Utf8Error(_, err) => Some(err),
444            }
445        }
446    }
447
448    pub fn read_stdin() -> String {
449        let mut buffer = String::new();
450        stdin()
451            .read_to_string(&mut buffer)
452            .map_err(|err| format!("Failed to read UTF-8 string from stdin due to: {}", err))
453            .unwrap();
454        buffer
455    }
456
457    pub fn read_stdin_bytes() -> Vec<u8> {
458        let mut buffer = Vec::new();
459        stdin()
460            .read_to_end(&mut buffer)
461            .map_err(|err| format!("Failed to read bytes from stdin due to: {}", err))
462            .unwrap();
463        buffer
464    }
465
466    pub fn read_stdin_lines() -> impl Iterator<Item = String> {
467        BufReader::new(stdin())
468            .lines()
469            .map(|val| val.map_err(|err| format!("Failed to read UTF-8 lines from stdin due to: {}", err)).unwrap())
470    }
471
472    /// Read content of all files as string.
473    pub fn read_all(paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Result<String, FileIoError> {
474        let mut string = String::new();
475
476        for path in paths {
477            let path = path.as_ref();
478            let mut file = File::open(path).map_err(|err| FileIoError::IoError(path.into(), err))?;
479            file.read_to_string(&mut string).map_err(|err| FileIoError::IoError(path.into(), err))?;
480        }
481
482        Ok(string)
483    }
484
485    /// Read content of all files as bytes.
486    pub fn read_all_bytes(paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Result<Vec<u8>, FileIoError> {
487        let mut bytes = Vec::new();
488
489        for path in paths {
490            let path = path.as_ref();
491            let mut file = File::open(path).map_err(|err| FileIoError::IoError(path.into(), err))?;
492            file.read_to_end(&mut bytes).map_err(|err| FileIoError::IoError(path.into(), err))?;
493        }
494
495        Ok(bytes)
496    }
497
498    #[cfg(all(feature = "clap", feature = "stderrlog"))]
499    #[derive(Args)]
500    pub struct ArgsLogger {
501        /// Verbose mode (-v for INFO, -vv for DEBUG)
502        #[arg(short = 'v', long, action = clap::ArgAction::Count)]
503        pub verbose: u8,
504
505        /// Quiet mode (-s for no WARN, -ss for no ERROR)
506        #[arg(short = 'q', long, action = clap::ArgAction::Count)]
507        quiet: u8,
508
509        /// Force colorizing the logger output
510        #[arg(long = "force-colors")]
511        pub force_colors: bool,
512    }
513
514    #[cfg(all(feature = "clap", feature = "stderrlog"))]
515    pub fn setup_logger(opt: ArgsLogger, module_paths: impl IntoIterator<Item = impl Into<String>>) {
516        let verbosity = (opt.verbose + 1) as i16 - opt.quiet as i16;
517        _setup_logger(verbosity, opt.force_colors, module_paths)
518    }
519
520    #[cfg(all(not(feature = "clap"), feature = "stderrlog"))]
521    pub fn setup_logger(verbosity: i16, force_colors: bool, module_paths: impl IntoIterator<Item = impl Into<String>>) {
522        _setup_logger(verbosity, force_colors, module_paths)
523    }
524
525    #[cfg(feature = "stderrlog")]
526    pub fn _setup_logger(verbosity: i16, force_colors: bool, module_paths: impl IntoIterator<Item = impl Into<String>>) {
527        let mut logger = stderrlog::new();
528
529        logger
530            .quiet(verbosity < 0)
531            .verbosity(verbosity as usize)
532            .color(if force_colors { stderrlog::ColorChoice::Always } else { stderrlog::ColorChoice::Auto })
533            .timestamp(stderrlog::Timestamp::Microsecond)
534            .module(module_path!())
535            .module("cotton")
536            .module("problem");
537
538        for module in module_paths {
539            logger.module(module);
540        }
541
542        logger
543            .init()
544            .unwrap();
545
546        #[cfg(feature = "problem")]
547        problem::format_panic_to_error_log();
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::prelude::*;
554
555    #[test]
556    #[should_panic(expected = "Failed to baz due to: while bar got error caused by: foo")]
557    fn test_problem() {
558        in_context_of("bar", || {
559            problem!("foo")?;
560            Ok(())
561        }).or_failed_to("baz");
562    }
563}