1use clap::Parser;
2use rustutils_runnable::Runnable;
3use std::env;
4use std::error::Error;
5use std::ffi::{OsStr, OsString};
6use std::io::Write;
7use std::os::unix::ffi::OsStrExt;
8use std::process::{Command, ExitStatus};
9
10#[derive(Parser, Clone, Debug)]
14#[clap(author, version, about, long_about = None)]
15pub struct Env {
16 #[clap(short, long)]
18 ignore_environment: bool,
19 #[clap(short = '0', long)]
21 null: bool,
22 #[clap(short, long, conflicts_with = "null")]
24 json: bool,
25 #[clap(short, long)]
27 unset: Vec<OsString>,
28 variables: Vec<OsString>,
30 #[clap(last = true)]
32 command: Vec<OsString>,
33}
34
35pub fn parse_variable(var: &OsStr) -> Option<(&OsStr, &OsStr)> {
40 let var = var.as_bytes();
41 var.iter()
42 .enumerate()
43 .skip(1)
44 .filter(|(_index, byte)| **byte == b'=')
45 .map(|(index, _byte)| index)
46 .last()
47 .map(|pos| {
48 (
49 OsStr::from_bytes(&var[..pos]),
50 OsStr::from_bytes(&var[pos + 1..]),
51 )
52 })
53}
54
55#[test]
56fn can_parse_variable() {
57 assert_eq!(parse_variable(OsStr::from_bytes(b"")), None);
58 assert_eq!(parse_variable(OsStr::from_bytes(b"=")), None);
59 assert_eq!(parse_variable(OsStr::from_bytes(b"VAR")), None);
60 assert_eq!(parse_variable(OsStr::from_bytes(b"=VAR")), None);
61 assert_eq!(
62 parse_variable(OsStr::from_bytes(b"VAR=value")),
63 Some((OsStr::from_bytes(b"VAR"), OsStr::from_bytes(b"value")))
64 );
65 assert_eq!(
66 parse_variable(OsStr::from_bytes(b"VAR=")),
67 Some((OsStr::from_bytes(b"VAR"), OsStr::from_bytes(b"")))
68 );
69}
70
71impl Env {
72 pub fn setup(&self) {
74 if self.ignore_environment {
76 for (name, _) in env::vars_os() {
77 env::remove_var(name);
78 }
79 }
80
81 for variable in &self.unset {
83 env::remove_var(variable);
84 }
85 }
86
87 pub fn parse_arguments(&self) -> Result<usize, Box<dyn Error>> {
88 let count = self
89 .variables
90 .iter()
91 .map(|variable| parse_variable(variable))
92 .take_while(|parsed| parsed.is_some())
93 .filter_map(|parsed| parsed)
94 .inspect(|(name, value)| env::set_var(name, value))
95 .count();
96 Ok(count)
97 }
98
99 pub fn run_command(&self, offset: usize) -> Result<Option<ExitStatus>, Box<dyn Error>> {
100 let mut args = self
101 .variables
102 .iter()
103 .skip(offset)
104 .chain(self.command.iter());
105 if let Some(arg) = args.next() {
106 let mut command = Command::new(arg);
107 let result = command.args(args).spawn()?.wait()?;
108 Ok(Some(result))
109 } else {
110 Ok(None)
111 }
112 }
113
114 pub fn run(&self) -> Result<Option<ExitStatus>, Box<dyn Error>> {
115 self.setup();
116 let offset = self.parse_arguments()?;
117 let result = self.run_command(offset)?;
118 if result.is_none() {
119 self.show()?;
120 }
121 Ok(result)
122 }
123
124 pub fn show(&self) -> Result<(), Box<dyn Error>> {
125 if self.json {
126 self.show_json()
127 } else {
128 let separator = match self.null {
129 true => OsStr::new("\0"),
130 false => OsStr::new("\n"),
131 };
132 self.show_normal(separator)
133 }
134 }
135
136 pub fn show_json(&self) -> Result<(), Box<dyn Error>> {
137 let mut environment: std::collections::BTreeMap<String, String> = Default::default();
138 for (name, value) in env::vars_os() {
139 if let (Some(name), Some(value)) = (name.to_str(), value.to_str()) {
140 environment.insert(name.to_string(), value.to_string());
141 }
142 }
143 let mut stdout = std::io::stdout();
144 stdout.write_all(serde_json::to_string(&environment)?.as_bytes())?;
145 Ok(())
146 }
147 pub fn show_normal(&self, separator: &OsStr) -> Result<(), Box<dyn Error>> {
148 let mut stdout = std::io::stdout();
149 for (name, value) in env::vars_os() {
150 stdout.write_all(name.as_bytes())?;
151 stdout.write_all(b"=")?;
152 stdout.write_all(value.as_bytes())?;
153 stdout.write_all(separator.as_bytes())?;
154 }
155 Ok(())
156 }
157}
158
159impl Runnable for Env {
160 fn run(&self) -> Result<(), Box<dyn Error>> {
161 self.run()?;
162 Ok(())
163 }
164}