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}