bash_builtins/variables/mod.rs
1//! This module contains functions to get, set, or unset shell variables.
2//!
3//! Use [`set`] and [`unset`] to modify shell variables.
4//!
5//! Use the `find` functions to access the value contained in existing shell
6//! variables. [`find_raw`] provides access to the raw pointer owned by bash,
7//! and both [`find`] and [`find_as_string`] provides a safe interface to such
8//! value.
9//!
10//! Use [`array_set`] and [`array_get`] to access the elements in an indexed
11//! array.
12//!
13//! Use [`assoc_get`] and [`assoc_get`] to access the elements in an associative
14//! array.
15//!
16//! ## Example
17//!
18//! The following example uses the shell variable `$SOMENAME_LIMIT` to set the
19//! configuration value for the builtin. If it is not present, or its value is
20//! not a valid `usize`, it uses a default value
21//!
22//! ```
23//! use bash_builtins::variables;
24//!
25//! const DEFAULT_LIMIT: usize = 1024;
26//!
27//! const LIMIT_VAR_NAME: &str = "SOMENAME_LIMIT";
28//!
29//! fn get_limit() -> usize {
30//! variables::find_as_string(LIMIT_VAR_NAME)
31//! .as_ref()
32//! .and_then(|v| v.to_str().ok())
33//! .and_then(|v| v.parse().ok())
34//! .unwrap_or(DEFAULT_LIMIT)
35//! }
36//! ```
37//!
38//! # Dynamic Variables
39//!
40//! Dynamic variables are shell variables that use custom functions each time
41//! they are accessed (like `$SECONDS` or `$RANDOM`).
42//!
43//! Use [`bind`] to create a dynamic variable with any type implementing
44//! [`DynamicVariable`].
45
46use crate::ffi::variables as ffi;
47use std::collections::HashMap;
48use std::ffi::{CStr, CString};
49use std::fmt;
50use std::os::raw::c_char;
51use std::process::ExitStatus;
52use std::ptr::{null, NonNull};
53
54#[cfg(unix)]
55use std::os::unix::process::ExitStatusExt;
56
57mod arrays;
58mod assoc;
59mod dynvars;
60
61pub use arrays::{array_get, array_set};
62pub use assoc::{assoc_get, assoc_set};
63pub use dynvars::DynamicVariable;
64
65/// Returns a string with the value of the shell variable `name`.
66///
67/// If the shell variable does not exist, or its value is an array, the function
68/// returns `None`.
69///
70/// # Example
71///
72/// ```no_run
73/// use bash_builtins::variables;
74///
75/// let var = variables::find_as_string("VAR_NAME");
76///
77/// if let Some(value) = var.as_ref().and_then(|v| v.to_str().ok()) {
78/// // `value` is a `&str` here.
79/// # let _ = value;
80/// } else {
81/// // `$VAR_NAME` is missing, an array, or contains
82/// // invalid UTF-8 data.
83/// }
84/// ```
85pub fn find_as_string(name: &str) -> Option<CString> {
86 unsafe { find_raw(name).and_then(|var| var.as_str().map(|cstr| cstr.to_owned())) }
87}
88
89/// Returns a copy of the value of the shell variable referenced by `name`.
90///
91/// If the shell variable does not exist, it returns `None`.
92///
93/// Use [`find_as_string`] if you want to skip arrays.
94pub fn find(name: &str) -> Option<Variable> {
95 unsafe { find_raw(name).map(|var| var.get()) }
96}
97
98/// Returns a reference to the address of the shell variable referenced by
99/// `name`.
100///
101/// Using this reference is unsafe because the memory is owned by bash. Whenever
102/// possible, use [`find`] or [`find_as_string`].
103pub fn find_raw(name: &str) -> Option<RawVariable> {
104 let name = CString::new(name).ok()?;
105 let shell_var = unsafe { ffi::find_variable(name.as_ptr()) as *mut _ };
106
107 NonNull::new(shell_var).map(RawVariable)
108}
109
110/// Sets the value of the shell variable referenced by `name`.
111///
112/// `value` is not required to be valid UTF-8, but it can't contain any nul
113/// byte.
114pub fn set<T>(name: &str, value: T) -> Result<(), VariableError>
115where
116 T: AsRef<[u8]>,
117{
118 let name = CString::new(name).map_err(|_| VariableError::InvalidName)?;
119 let value = CString::new(value.as_ref()).map_err(|_| VariableError::InvalidValue)?;
120
121 let res = unsafe {
122 if ffi::legal_identifier(name.as_ptr()) == 0 {
123 return Err(VariableError::InvalidName);
124 }
125
126 ffi::bind_variable(name.as_ptr(), value.as_ptr(), 0)
127 };
128
129 if res.is_null() {
130 Err(VariableError::InvalidValue)
131 } else {
132 Ok(())
133 }
134}
135
136/// Unset the shell variable referenced by `name`.
137///
138/// Returns `true` if the shell variable is removed.
139pub fn unset(name: &str) -> bool {
140 let name = match CString::new(name) {
141 Ok(s) => s,
142 Err(_) => return false,
143 };
144
145 unsafe { ffi::unbind_variable(name.as_ptr()) == 0 }
146}
147
148/// Bind the shell variable referenced by `name` to an instance of
149/// [`DynamicVariable`].
150///
151/// See the documentation of [`DynamicVariable`] for details on how to define a
152/// dynamic variable.
153pub fn bind(name: &str, dynvar: impl DynamicVariable + 'static) -> Result<(), VariableError> {
154 dynvars::bind_dynvar(name, Box::new(dynvar) as Box<dyn DynamicVariable>)
155}
156
157/// Return a copy of the last command's exit status.
158#[cfg(unix)]
159pub fn get_last_exit_status() -> ExitStatus {
160 let raw_exit_code = unsafe { ffi::get_exitstat(null()) };
161
162 ExitStatus::from_raw(raw_exit_code)
163}
164
165/// An error from a shell variable operation, like [`set`] or [`bind`].
166#[derive(Debug)]
167pub enum VariableError {
168 InvalidName,
169 InvalidValue,
170 NotAssocArray,
171 InternalError(&'static str),
172}
173
174impl fmt::Display for VariableError {
175 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
176 match self {
177 VariableError::InvalidName => fmt.write_str("invalid variable name"),
178 VariableError::InvalidValue => fmt.write_str("invalid variable value"),
179 VariableError::NotAssocArray => fmt.write_str("variable is not an associative array"),
180 VariableError::InternalError(cause) => write!(fmt, "internal error: {}", cause),
181 }
182 }
183}
184
185impl std::error::Error for VariableError {}
186
187/// Contains the value of a shell variable.
188///
189/// Use [`find`] or [`RawVariable::get`] to get this value.
190///
191/// # Example
192///
193/// A function to print the value of `var`.
194///
195/// ```
196/// use bash_builtins::variables::Variable;
197/// use std::io::{self, Write};
198///
199/// fn print<W>(mut output: W, name: &str, var: &Variable) -> io::Result<()>
200/// where
201/// W: Write,
202/// {
203/// match var {
204/// Variable::Str(s) => {
205/// writeln!(output, "{} = {:?}", name, s)?;
206/// }
207///
208/// Variable::Array(a) => {
209/// for (idx, elem) in a.iter().enumerate() {
210/// writeln!(&mut output, "{}[{}] = {:?}", name, idx, elem)?;
211/// }
212/// }
213///
214/// Variable::Assoc(a) => {
215/// for (key, value) in a.iter() {
216/// writeln!(&mut output, "{}[{:?}] = {:?}", name, key, value)?;
217/// }
218/// }
219/// }
220///
221/// Ok(())
222/// }
223/// ```
224#[derive(Debug)]
225pub enum Variable {
226 /// A single string.
227 Str(CString),
228
229 /// An indexed [array](https://www.gnu.org/software/bash/manual/html_node/Arrays.html).
230 ///
231 /// Each element is a tuple with the index and the value of the items in
232 /// the array.
233 Array(Vec<(i64, CString)>),
234
235 /// An associative [array](https://www.gnu.org/software/bash/manual/html_node/Arrays.html).
236 ///
237 /// These shell variables are initialized with `declare -A`.
238 Assoc(HashMap<CString, CString>),
239}
240
241/// Raw reference to a shell variable.
242///
243/// Every method is unsafe because this type contains a raw pointer to an
244/// address owned by bash.
245///
246/// Whenever possible, use [`find`] or [`find_as_string`] functions to get the
247/// value of a shell variable.
248#[derive(Debug)]
249pub struct RawVariable(NonNull<ffi::ShellVar>);
250
251impl RawVariable {
252 /// Returns `true` if the shell variable contains an indexed array.
253 ///
254 /// # Safety
255 ///
256 /// This method is unsafe because it does not check that the address of the
257 /// shell variable is still valid.
258 pub unsafe fn is_array(&self) -> bool {
259 self.0.as_ref().attributes & ffi::ATT_ARRAY != 0
260 }
261
262 /// Returns `true` if the shell variable contains an associative array.
263 ///
264 /// # Safety
265 ///
266 /// This method is unsafe because it does not check that the address of the
267 /// shell variable is still valid.
268 pub unsafe fn is_assoc(&self) -> bool {
269 self.0.as_ref().attributes & ffi::ATT_ASSOC != 0
270 }
271
272 /// Extracts the contents of the shell variable, and returns a copy of the it.
273 ///
274 /// # Safety
275 ///
276 /// This method is unsafe because it does not check that the address of the
277 /// shell variable is still valid.
278 pub unsafe fn get(&self) -> Variable {
279 unsafe fn cstr(addr: *const c_char) -> CString {
280 CStr::from_ptr(addr).to_owned()
281 }
282
283 if self.is_assoc() {
284 let items = self.assoc_items().map(|(k, v)| (cstr(k), cstr(v)));
285 Variable::Assoc(items.collect())
286 } else if self.is_array() {
287 Variable::Array(self.array_items())
288 } else {
289 Variable::Str(cstr(self.0.as_ref().value))
290 }
291 }
292
293 /// Returns a reference to the string contained in the shell variable. If
294 /// the shell variable contains an array, returns `None`.
295 ///
296 /// # Safety
297 ///
298 /// This method is unsafe for two reasons:
299 ///
300 /// * It does not check that the address of the shell variable is still
301 /// valid.
302 /// * The `CStr` reference is wrapping a pointer managed by bash, so its
303 /// lifetime is not guaranteed.
304 pub unsafe fn as_str(&self) -> Option<&CStr> {
305 let var = self.0.as_ref();
306 if var.attributes & (ffi::ATT_ARRAY | ffi::ATT_ASSOC) == 0 {
307 Some(CStr::from_ptr(var.value))
308 } else {
309 None
310 }
311 }
312
313 /// Returns a vector with the items of the indexed array contained in the
314 /// variable.
315 ///
316 /// Each item in the vector is the index of the array and its value.
317 ///
318 /// # Safety
319 ///
320 /// This method is unsafe for two reasons:
321 ///
322 /// * It does not check that the address of the shell variable is still
323 /// valid.
324 /// * It does not check that the shell variable contains an indexed array.
325 pub unsafe fn array_items(&self) -> Vec<(i64, CString)> {
326 arrays::array_items(self.0.as_ref())
327 }
328
329 /// Returns an iterator over items of the associative array contained in
330 /// the shell variable.
331 ///
332 /// # Safety
333 ///
334 /// This method is unsafe for two reasons:
335 ///
336 /// * It does not check that the address of the shell variable is still
337 /// valid.
338 /// * It does not check that the shell variable contains an associative
339 /// array.
340 pub unsafe fn assoc_items(&self) -> impl Iterator<Item = (*const c_char, *const c_char)> + '_ {
341 let table = &*(self.0.as_ref().value as *const ffi::HashTable);
342 assoc::AssocItemsIterator::new(table)
343 }
344}