pluribus 0.2.0

Small crate providing a macro to create multicall binaries declaratively.
Documentation
// Copyright 2025 Guilherme Duarte Silva Matos

//! # pluribus
//! A small macro crate to make multicall binaries easier!
//!
//! # What is a multicall binary?
//! Multicall binaries are simply single executables that
//! can behave as many different executables.
//!
//! Good and famous examples of such a multicall binary would
//! be [`busybox`](https://www.busybox.net/)
//! and [`uutils`](https://uutils.github.io/coreutils/docs/index.html).
//!
//! # What is included?
//! This crate provides a single macro, ```pluribus!()```,
//! with a special, easy to read and write DSL (special syntax) that
//! declaratively turns your modules into separate applications.
//!
//! # Hmm... not quite sure I understood...
//! Well, no biggie! That's what examples are for!
//! ```rust
//! use pluribus::pluribus;
//! // Declaring a binary 'greet'.
//! mod greet {
//!     // Start is our start symbol!
//!     pub fn start(args: &[String]) {
//!         for s in &args[1..] {
//!             println!("Greetings, {s}!");
//!         }
//!     }
//! }
//!
//! // Declaring a binary 'bye'
//! mod bye {
//!     // Start is our start symbol!
//!     pub fn start(args: &[String]) {
//!         for s in &args[1..] {
//!             println!("Goodbye, {s}!");
//!         }
//!     }
//! }
//!
//! mod rust_out {
//!    pub fn start(args: &[String]) {
//!       println!("Success!");
//!    }
//! }
//!
//!
//! fn main() -> std::io::Result<()> {
//!     // symbol: <our function that starts a binary, defaulting to main>;
//!     // returning: <return type of the binaries>;
//!     // with: <list of all modules we wish to turn into binaries>
//!     pluribus!(
//!         symbol: start;
//!         returns: ();
//!         with:
//!         - greet;
//!         - bye;
//!         - rust_out;
//!     )
//!     (&std::env::args().collect::<Vec<_>>());
//!     Ok(())
//! }
//! ```
//! This small program, let's call it 'test', now includes both the `greet` and `bye`
//! binaries!
//!
//! Now, how are they accessed? Like this:
//! ```sh
//! > ./test greet Alice Bob
//! Greetings, Alice!
//! Greetings, Bob!
//! ```
//! Or, alternatively, we can create a symlink or rename the binary:
//! ```sh
//! > ln -s test greet
//! > ./greet Alice Bob
//! Greetings, Alice!
//! Greetings, Bob!
//! ```
//! # The syntax
//! Admittedly, it is a little bit dumb and too strict,
//! but because it is so simple, it should prove easy to use.
//!
//! There are three directives in the language, and they must be
//! stated in order.
//!
//! They are as follows:
//! - [symbol](#symbol)
//! - [returns](#returns)
//! - [with](#with)
//!
//! ## symbol
//! `symbol` specifies the main symbol of the sub-binary, eg. `main`, `_start`.
//! ```text
//! symbol: main;
//! ```
//! It defaults to `main`. If you are using `main` as the symbol, you may omit this.
//! ## returns
//! `returns` specifies the return type of the sub-binary, eg `()`, `std::io::Result<()>`.
//! ```text
//! returns: ();
//! ```
//! It defaults to `()`. If you are using `()` as the return type, you may omit this.
//! ## with
//! `with` specifies the beginning of the list of modules/sub-binaries. It must be the
//! last (or only) [directive](#the-syntax).
//! ```text
//! with:
//!   - bin1;
//!   - bin2;
//!   - bin3;
//! ```

/// Creates a function that runs a dispatcher for the binaries.
/// ```rust,ignore
/// mod greet;
/// mod bye;
/// pluribus!(
///     symbol: start;
///     returns: ();
///     with:
///     #[cfg(feature = "greeting")]
///     - greet;
///     #[cfg(target_family = "unix")]
///     - bye;
/// )
/// ```
#[macro_export]
macro_rules! pluribus {
    (symbol: $sym:ident; with: $($(#[$cfg:meta])*-$bin:ident;)+) => {
         pluribus!(
            symbol: $sym;
            returns: ();
            with: $($(#[$cfg])*-$bin;)+
        )
    };
    (with: $($(#[$cfg:meta])* -$bin:ident;)+) => {
        pluribus!(
            returns: ();
            with: $($(#[$cfg])*-$bin;)+
        )
    };
    (returns: $output:ty; with: $($(#[$cfg:meta])*-$bin:ident;)+) => {
        pluribus!(
            symbol: main;
            returns: $output;
            with: $($(#[$cfg])*-$bin;)+
        )
    };
    (symbol: $sym:ident; returns: $output:ty; with: $($(#[$cfg:meta])*-$bin:ident;)+) => {
        |argv: &[String]| -> $output {
            let pkg_name = env!("CARGO_PKG_NAME");
            let (name, args) = match argv.len() {
                0 => {
                    eprintln!("Error: Argument buffer is empty. Aborting...");
                    std::process::exit(1);
                }
                1 => ($crate::__basename(&argv[0]), argv),
                _ => if $crate::__basename(&argv[0]) == pkg_name {
                    ($crate::__basename(&argv[1]), &argv[1..])
                } else {
                    ($crate::__basename(&argv[0]), argv)
                },
            };

            match name.as_str() {
                $(
                $(#[$cfg])*
                stringify!($bin) => $bin::$sym(args),
                )+
                n => {
                    let main_binary_name = pkg_name.to_string();
                    let target_binary_name = n.to_string();
                    let available_binaries = $crate::__prettify_bins(&[
                            $(
                                $(#[$cfg])*
                                stringify!($bin),
                            )+
                        ]
                    );
                    eprintln!("Error: '{main_binary_name}': no such binary '{target_binary_name}'. Try instead one of: {available_binaries}");
                    std::process::exit(1);
                }
            }
        }
    };
}

#[doc(hidden)]
pub fn __prettify_bins(v: &[&str]) -> String {
    if v.is_empty() {
        return "(none).".to_string();
    }

    if v.len() == 1 {
        return format!("'{}'.", v[0]);
    }
    if v.len() == 2 {
        return format!("'{}' or '{}'.", v[0], v[1]);
    }
    let mut res = String::new();
    let v_len = v.len();
    for (i, s) in v.into_iter().enumerate() {
        if i + 1 == v_len {
            res += &format!("or '{s}'.");
        } else {
            res += &format!("'{s}', ");
        }
    }
    res
}

#[doc(hidden)]
pub fn __basename(s: &str) -> String {
    s.split(|x| x == '/' || x == '\\')
        .filter(|a| !a.is_empty())
        .last()
        .unwrap_or(s)
        .to_string()
}