1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
// SPDX-License-Identifier: GPL-3.0-or-later

//! 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;
    }

    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;
    }

    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;