rust_script_ext/
lib.rs

1//! Opinionated set of extensions for use with
2//! [`rust-script`](https://github.com/fornwall/rust-script).
3
4//! Using `rust-script` to run Rust like a shell script is great!
5//! This crate provides an opinionated set of extensions tailored towards common patterns in scripts.
6//! These patterns include file reading, argument parsing, error handling.
7//!
8//! # Argument Parsing
9//!
10//! A rudimentary argument parser is provided, simply call [`args`](args::args).
11//!
12//! The parsing is meant to be simple, tailored to script usage. For fully featured CLI apps,
13//! consider importing [`clap`](https://docs.rs/clap/latest/clap/index.html).
14//!
15//! # Error Handling
16//!
17//! Error handling uses the [`anyhow`] crate.
18//! A `Result` type alias is exposed, [`Context`](anyhow::Context) can be added.
19//!
20//! ```rust
21//! # use rust_script_ext::prelude::*;
22//! fn foo() -> Result<String> {
23//!    std::fs::read_to_string("foo.txt")
24//!        .context("failed to open 'foo.txt'")
25//! }
26//! ```
27//!
28//! # Invoking Commands
29//!
30//! Running commands is done through `std::process::Command`.
31//! There are a few helper traits and macros to assist in:
32//!
33//! 1. Building a `Command`, and
34//! 2. Executing a command.
35//!
36//! Building commands can leverage the [`cmd!`](crate::prelude::cmd) macro.
37//! This can be used to succintly build a command with arguments.
38//!
39//! ```rust
40//! # use rust_script_ext::prelude::*;
41//! let x = 1.0;
42//! let cmd = cmd!(./my-script.sh: foo/bar, --verbose, {x + 2.14});
43//! assert_eq!(&cmd.cmd_str(), "./my-script.sh foo/bar --verbose 3.14");
44//! ```
45//!
46//! The [`CommandExecute`](crate::prelude::CommandExecute) trait provides some methods which
47//! can execute a command and automatically collect the output, along with providing verbose
48//! error messages if something fails.
49//!
50//! ```rust,no_run
51//! # use rust_script_ext::prelude::*;
52//! // Verbose means also print stdout/stderr to terminal as execution occurs
53//! cmd!(ls: src).execute_str(Verbose).unwrap();
54//! ```
55//!
56//! # Serialisation
57//!
58//! [`Serialize`](::serde::Serialize), [`Deserialize`](::serde::Deserialize),
59//! and [`DeserializeOwned`](::serde::de::DeserializeOwned) are all exposed.
60//! Because of some path quirks with re-exported proc-macros, all derived values need to be tagged
61//! with the path to the serde crate, as shown below.
62//!
63//! ```rust
64//! # use rust_script_ext::prelude::*;
65//! #[derive(Deserialize)]
66//! #[serde(crate = "deps::serde")]
67//! struct PizzaOrder {
68//!    ham: bool,
69//!    cheese: bool,
70//!    pineapple: bool,
71//! }
72//! ```
73//!
74//! # Structured IO
75//!
76//! A common pattern is to read/write with a particular serialisation format.
77//! Examples include reading a CSV file from disk, or writing JSON to stdout.
78//! An abstraction is provided (structured IO) which produces
79//! [`read_as`](crate::prelude::ReadAs) and [`write_as`](crate::prelude::WriteAs) functions on
80//! typical targets so working with structured data is ergonomic.
81//!
82//! ```rust
83//! # use rust_script_ext::prelude::*;
84//! #[derive(Serialize, Deserialize, Debug, PartialEq)]
85//! struct City {
86//!     city: String,
87//!     pop: u32,
88//! }
89//!
90//! let csv = "city,pop\nBrisbane,100000\nSydney,200000\n";
91//!
92//! // read_as on anything that is Read
93//! let x = csv.as_bytes().read_as::<CSV, City>().unwrap();
94//!
95//! assert_eq!(
96//!     x,
97//!     vec![
98//!         City {
99//!             city: "Brisbane".to_string(),
100//!             pop: 100_000,
101//!         },
102//!         City {
103//!             city: "Sydney".to_string(),
104//!             pop: 200_000,
105//!         }
106//!     ]
107//! );
108//!
109//! let mut buf = Vec::new();
110//!
111//! // write &[T] (T: Serialize) as a CSV into a Writer
112//! x.write_as(CSV, &mut buf).unwrap();
113//! assert_eq!(buf, csv.as_bytes());
114//! ```
115//!
116//! # Date and Time
117//!
118//! Date and time is handled by exposing the [`time`](::time) crate.
119//! For _duration_, [`humantime`](::humantime) is used, exposing its `Duration` directly. This is
120//! done for duration parsing similar to what is experienced in unix tools.
121//!
122//! # Number formatting
123//!
124//! [`numfmt::Formatter`] is exposed (as [`NumFmt`](prelude::NumFmt)) which can be used
125//! to format numbers in a nice way. The `numfmt` module documentation describes ways to
126//! build a formatter, along with the syntax for parsing a format string.
127//!
128//! # Progress reporting
129//!
130//! [`how-u-doin`](::howudoin) can be used to show progress of a longer running script.
131//!
132//! # Tabular printing
133//!
134//! Tables can be printed neatly with [`TablePrinter`](prelude::TablePrinter), which is just
135//! exposing [`comfy-table`](::comfy_table).
136#![warn(missing_docs)]
137
138mod args;
139mod cmd;
140mod fs;
141mod io;
142
143/// Exposed dependency crates.
144pub mod deps {
145    pub use ::anyhow;
146    pub use ::comfy_table;
147    pub use ::csv;
148    pub use ::fastrand;
149    pub use ::flume;
150    pub use ::globset;
151    pub use ::howudoin;
152    pub use ::humantime;
153    pub use ::itertools;
154    pub use ::numfmt;
155    pub use ::rayon;
156    pub use ::regex;
157    pub use ::serde;
158    pub use ::serde_json;
159    pub use ::time;
160    pub use ::toml;
161}
162
163/// Typical imports.
164pub mod prelude {
165    pub use super::deps;
166
167    pub use super::args::{args, Args};
168
169    pub use super::cmd::{
170        CommandBuilder, CommandExecute, CommandString,
171        Output::{self, *},
172    };
173
174    pub use ::comfy_table::{self, Table as TablePrinter};
175
176    /// CSV [`Reader`](::csv::Reader) backed by a [`File`](super::fs::File).
177    pub type CsvReader = ::csv::Reader<super::fs::File>;
178
179    /// CSV [`Writer`](::csv::Writer) backed by a [`File`](super::fs::File).
180    pub type CsvWriter = ::csv::Writer<super::fs::File>;
181
182    pub use super::fs::{ls, File};
183    pub use super::io::{Format, ReadAs, WriteAs, CSV, JSON, TOML};
184    pub use ::anyhow::{anyhow, bail, ensure, Context, Error, Result};
185    pub use ::fastrand;
186    pub use ::flume::{bounded, unbounded, Receiver, Sender};
187    pub use ::howudoin;
188    pub use ::humantime::{parse_duration, Duration, Timestamp};
189    pub use ::itertools::Itertools;
190    pub use ::numfmt::Formatter as NumFmt;
191    pub use ::rayon;
192    pub use ::regex::Regex;
193    pub use ::serde::{de::DeserializeOwned, Deserialize, Serialize};
194    pub use ::serde_json::Value as JsonValue;
195    pub use ::time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};
196    pub use std::io::{Read, Write};
197
198    // publically document cargs! and cmd! here
199
200    /// Construct a `[String]` array from a list of arguments.
201    ///
202    /// This macro is primarily for use with [`cmd!`](cmd), but can also be independently
203    /// used, a great location is [`Command::args`](std::process::Command::args).
204    ///
205    /// Arguments are delimited by commas, any text between delimiters is stringified and
206    /// passed through.
207    /// Arguments wrapped in braces (`{ ... }`) are treated as expressions to be evaluated.
208    /// This effectively writes `{ ... }.to_string()`.
209    ///
210    /// ```plaintext
211    /// arg1, arg2/foo, {expr}
212    /// ```
213    ///
214    /// # Example
215    /// ```rust
216    /// # use rust_script_ext::prelude::*;
217    ///
218    /// let x = "hello";
219    /// let c = cargs!(foo, bar/zog, {x}, {1 + 2});
220    /// assert_eq!(c, [
221    ///     "foo".to_string(),
222    ///     "bar/zog".to_string(),
223    ///     "hello".to_string(),
224    ///     "3".to_string()
225    /// ]);
226    /// ```
227    pub use ::macros::cargs;
228
229    /// Helper to construct a [`Command`] with arguments.
230    ///
231    /// The macro uses the syntax:
232    /// ```plaintext
233    /// cmd: arg1, arg2
234    /// ```
235    ///
236    /// That is, the command path, optionally followed by a colon (`:`) followed by one or
237    /// more _comma delimited_ arguments.
238    ///
239    /// Note that `cmd!` defers to [`cargs!`](cargs) to parse the arguments.
240    ///
241    /// The macro is powerful enough to support raw path identifiers:
242    /// ```rust
243    /// # use rust_script_ext::prelude::*;
244    /// let c = cmd!(ls); // no args
245    /// assert_eq!(&c.cmd_str(), "ls");
246    ///
247    /// let c = cmd!(ls: foo/bar, zog);
248    /// assert_eq!(&c.cmd_str(), "ls foo/bar zog");
249    ///
250    /// let c = cmd!(./local-script.sh: foo/bar, zog);
251    /// assert_eq!(&c.cmd_str(), "./local-script.sh foo/bar zog");
252    /// ```
253    ///
254    /// Literals are supported:
255    /// ```rust
256    /// # use rust_script_ext::prelude::*;
257    /// let c = cmd!(ls: "foo bar", 1.23);
258    /// assert_eq!(&c.cmd_str(), r#"ls "foo bar" 1.23"#);
259    /// ```
260    ///
261    /// Arguments wrapped in braces (`{ ... }`) are treated as expressions to be evaluated.
262    /// This effectively writes `{ ... }.to_string()`.
263    ///
264    /// ```rust
265    /// # use rust_script_ext::prelude::*;
266    /// let h = "hello";
267    /// let c = cmd!(ls: {h}, {format!("world")});
268    /// assert_eq!(&c.cmd_str(), "ls hello world");
269    /// ```
270    ///
271    /// [`Command`]: std::process::Command
272    pub use ::macros::cmd;
273}
274
275#[cfg(test)]
276fn pretty_print_err(err: anyhow::Error) -> String {
277    format!("{err:#}")
278}