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}