cnf_lib/util/command_line.rs
1// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
2// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//! Helpers for working with command lines.
6//!
7//! Provides [`CommandLine`] and the accompanying macro [`cmd!`], which abstract the trouble of
8//! assembling command lines for [`Environment`s](crate::env::Environment). Most importantly,
9//! [`CommandLine`] offers an [`Environment`](crate::env::Environment)-agnostic way of requesting
10//! privileges for command execution. This is useful when interacting with system package managers,
11//! for example, which often require root privileges for installation or updating caches.
12//!
13//! See [`CommandLine::privileged`] for more information.
14use serde_derive::{Deserialize, Serialize};
15use std::fmt;
16
17/// Convenient container for commandlines to execute in an [`crate::env::Environment`].
18#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq)]
19pub struct CommandLine {
20 /// The commandline to execute
21 cmd: Vec<String>,
22 /// True if execution requires privileges, false otherwise. If true and `interactive` is false,
23 /// execution may fail due to a lack of authentication.
24 #[serde(default)]
25 privileged: bool,
26 /// If set to true, privileges may be acquired interactively (e.g. using password prompts). If
27 /// false, privileges must be acquired non-internactively, or not at all.
28 #[serde(default)]
29 interactive: bool,
30}
31
32impl CommandLine {
33 /// Create a new instance of CommandLine.
34 ///
35 /// The given slice must contain at least one entry, otherwise a runtime error occurs.
36 pub fn new<S: Clone + Into<String>>(cmd: &[S]) -> Self {
37 debug_assert!(!cmd.is_empty(), "commandline mustn't be empty");
38
39 let as_vec = cmd
40 .iter()
41 .map(|s| Into::<String>::into(s.clone()))
42 .collect::<Vec<String>>();
43 Self {
44 cmd: as_vec,
45 privileged: false,
46 interactive: false,
47 }
48 }
49
50 /// True if nothing has been added to the commandline yet.
51 pub fn is_empty(&self) -> bool {
52 self.cmd.is_empty()
53 }
54
55 /// Request privileges for [`CommandLine`] on-the-fly.
56 ///
57 /// See [`CommandLine::needs_privileges`].
58 pub fn privileged(mut self) -> Self {
59 self.needs_privileges(true);
60 self
61 }
62
63 /// Control whether privileges are needed to execute the command.
64 ///
65 /// Privilege escalation is handled by each respective [`crate::env::Environment`] and on Unix
66 /// systems usually refers to some way of executing commands as the `root` user.
67 pub fn needs_privileges(&mut self, yesno: bool) {
68 self.privileged = yesno;
69 }
70
71 pub fn get_privileged(&self) -> bool {
72 self.privileged
73 }
74
75 /// Control whether command execution should be interactive.
76 pub fn is_interactive(&mut self, yesno: bool) {
77 self.interactive = yesno;
78 }
79
80 pub fn get_interactive(&self) -> bool {
81 self.interactive
82 }
83
84 /// Append arguments to an existing commandline.
85 pub fn append<S: Clone + Into<String>>(&mut self, args: &[S]) {
86 args.iter()
87 .for_each(|arg| self.cmd.push(Into::<String>::into(arg.clone())))
88 }
89
90 /// Obtain the command (first "chunk") of the commandline.
91 pub fn command(&self) -> String {
92 self.cmd[0].clone()
93 }
94
95 /// Obtain the arguments of the commandline.
96 ///
97 /// Returns an empty slice if there are no arguments in the command line.
98 pub fn args(&self) -> &[String] {
99 self.cmd.get(1..).unwrap_or(&[])
100 }
101}
102
103impl fmt::Display for CommandLine {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "{} {}", self.command(), self.args().join(" "))
106 }
107}
108
109impl<S: Clone + Into<String>> From<Vec<S>> for CommandLine {
110 fn from(value: Vec<S>) -> Self {
111 Self::new(&value)
112 }
113}
114
115/// Conveniently create [`CommandLine`]s.
116#[macro_export]
117macro_rules! cmd {
118 ( $( $text:expr ),+ ) => {
119 CommandLine::new( &[ $( $text ),+ ] )
120 };
121}
122pub use cmd;