cnf-lib 0.6.0

Distribution-agnostic 'command not found'-handler
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
// This file is part of cnf-lib, available at <https://gitlab.com/hartang/rust/cnf>

//! # Helpers for working with command lines
//!
//! Provides [`CommandLine`] and the accompanying macro [`cmd!`], which abstract the trouble of
//! assembling command lines for [`Environment`s](crate::env::Environment). Most importantly,
//! [`CommandLine`] offers an [`Environment`](crate::env::Environment)-agnostic way of requesting
//! privileges for command execution. This is useful when interacting with system package managers,
//! for example, which often require root privileges for installation or updating caches.
//!
//! See [`CommandLine::privileged`] for more information.
use serde_derive::{Deserialize, Serialize};
use std::fmt;

/// Convenient container for commandlines to execute in an [`crate::env::Environment`].
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct CommandLine {
    /// The commandline to execute
    cmd: Vec<String>,
    /// True if execution requires privileges, false otherwise. If true and `interactive` is false,
    /// execution may fail due to a lack of authentication.
    #[serde(default)]
    privileged: bool,
    /// If set to true, privileges may be acquired interactively (e.g. using password prompts). If
    /// false, privileges must be acquired non-internactively, or not at all.
    #[serde(default)]
    interactive: bool,
}

impl CommandLine {
    /// Create a new instance of CommandLine.
    ///
    /// The given slice must contain at least one entry, otherwise a runtime error occurs.
    pub fn new<S: Clone + Into<String>>(cmd: &[S]) -> Self {
        debug_assert!(!cmd.is_empty(), "commandline mustn't be empty");

        let as_vec = cmd
            .iter()
            .map(|s| Into::<String>::into(s.clone()))
            .collect::<Vec<String>>();
        Self {
            cmd: as_vec,
            privileged: false,
            interactive: false,
        }
    }

    /// True if nothing has been added to the commandline yet.
    pub fn is_empty(&self) -> bool {
        self.cmd.is_empty()
    }

    /// Request privileges for [`CommandLine`] on-the-fly.
    ///
    /// See [`CommandLine::needs_privileges`].
    pub fn privileged(mut self) -> Self {
        self.needs_privileges(true);
        self
    }

    /// Control whether privileges are needed to execute the command.
    ///
    /// Privilege escalation is handled by each respective [`crate::env::Environment`] and on Unix
    /// systems usually refers to some way of executing commands as the `root` user.
    pub fn needs_privileges(&mut self, yesno: bool) {
        self.privileged = yesno;
    }

    /// Tell whether privileges are needed to execute the command.
    pub fn get_privileged(&self) -> bool {
        self.privileged
    }

    /// Control whether command execution should be interactive.
    pub fn is_interactive(&mut self, yesno: bool) {
        self.interactive = yesno;
    }

    /// Tell if the command execution should be interactive.
    pub fn get_interactive(&self) -> bool {
        self.interactive
    }

    /// Append arguments to an existing commandline.
    pub fn append<S: Clone + Into<String>>(&mut self, args: &[S]) {
        args.iter()
            .for_each(|arg| self.cmd.push(Into::<String>::into(arg.clone())))
    }

    /// Obtain the command (first "chunk") of the commandline.
    pub fn command(&self) -> String {
        self.cmd[0].clone()
    }

    /// Obtain the arguments of the commandline.
    ///
    /// Returns an empty slice if there are no arguments in the command line.
    pub fn args(&self) -> &[String] {
        self.cmd.get(1..).unwrap_or(&[])
    }
}

impl fmt::Display for CommandLine {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.command(), self.args().join(" "))
    }
}

impl<S: Clone + Into<String>> From<Vec<S>> for CommandLine {
    fn from(value: Vec<S>) -> Self {
        Self::new(&value)
    }
}

/// Conveniently create [`CommandLine`]s.
#[macro_export]
macro_rules! cmd {
    ( $( $text:expr ),+ ) => {
        CommandLine::new( &[ $( $text ),+ ] )
    };
}
pub use cmd;