scallop/
builtins.rs

1use std::borrow::Borrow;
2use std::collections::HashSet;
3use std::ffi::{CStr, CString, c_int};
4use std::hash::{Hash, Hasher};
5use std::ops::Deref;
6use std::{cmp, fmt, mem, process, ptr};
7
8use bitflags::bitflags;
9
10use crate::macros::*;
11use crate::traits::IntoWords;
12use crate::{Error, ExecStatus, bash, shell};
13
14mod _bash;
15pub mod profile;
16
17// export native bash builtins
18pub use _bash::*;
19
20pub type BuiltinFn = fn(&[&str]) -> crate::Result<ExecStatus>;
21pub type BuiltinFnPtr = unsafe extern "C" fn(list: *mut bash::WordList) -> c_int;
22
23bitflags! {
24    /// Flag values describing builtin attributes.
25    pub struct Attr: u32 {
26        const NONE = 0;
27        const ENABLED = bash::BUILTIN_ENABLED;
28        const DELETED = bash::BUILTIN_DELETED;
29        const STATIC = bash::STATIC_BUILTIN;
30        const SPECIAL = bash::SPECIAL_BUILTIN;
31        const ASSIGNMENT = bash::ASSIGNMENT_BUILTIN;
32        const POSIX = bash::POSIX_BUILTIN;
33        const LOCALVAR = bash::LOCALVAR_BUILTIN;
34        const ARRAYREF = bash::ARRAYREF_BUILTIN;
35    }
36}
37
38pub mod set {
39    use super::*;
40
41    pub fn enable<S: AsRef<str>>(opts: &[S]) -> crate::Result<ExecStatus> {
42        set(["-o"].into_iter().chain(opts.iter().map(|s| s.as_ref())))
43    }
44
45    pub fn disable<S: AsRef<str>>(opts: &[S]) -> crate::Result<ExecStatus> {
46        set(["+o"].into_iter().chain(opts.iter().map(|s| s.as_ref())))
47    }
48}
49
50pub mod shopt {
51    use super::*;
52
53    pub fn enable<S: AsRef<str>>(opts: &[S]) -> crate::Result<ExecStatus> {
54        shopt(["-s"].into_iter().chain(opts.iter().map(|s| s.as_ref())))
55    }
56
57    pub fn disable<S: AsRef<str>>(opts: &[S]) -> crate::Result<ExecStatus> {
58        shopt(["-u"].into_iter().chain(opts.iter().map(|s| s.as_ref())))
59    }
60}
61
62#[derive(Clone, Copy)]
63pub struct Builtin {
64    pub name: &'static str,
65    pub func: BuiltinFn,
66    pub flags: u32,
67    pub cfunc: BuiltinFnPtr,
68    pub help: &'static str,
69    pub usage: &'static str,
70}
71
72impl fmt::Debug for Builtin {
73    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74        f.debug_struct("Builtin").field("name", &self.name).finish()
75    }
76}
77
78impl fmt::Display for Builtin {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        write!(f, "{}", self.name)
81    }
82}
83
84impl PartialEq for Builtin {
85    fn eq(&self, other: &Self) -> bool {
86        self.name == other.name
87    }
88}
89
90impl Eq for Builtin {}
91
92impl Hash for Builtin {
93    fn hash<H: Hasher>(&self, state: &mut H) {
94        self.name.hash(state);
95    }
96}
97
98impl Ord for Builtin {
99    fn cmp(&self, other: &Self) -> cmp::Ordering {
100        self.name.cmp(other.name)
101    }
102}
103
104impl PartialOrd for Builtin {
105    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
106        Some(self.cmp(other))
107    }
108}
109
110impl AsRef<str> for Builtin {
111    fn as_ref(&self) -> &str {
112        self.name
113    }
114}
115
116impl Borrow<str> for Builtin {
117    fn borrow(&self) -> &str {
118        self.name
119    }
120}
121
122// TODO: replace with callable trait implementation if it's ever stabilized
123// https://github.com/rust-lang/rust/issues/29625
124impl Deref for Builtin {
125    type Target = BuiltinFn;
126
127    fn deref(&self) -> &Self::Target {
128        &self.func
129    }
130}
131
132/// Convert a Builtin to its C equivalent.
133impl From<Builtin> for bash::Builtin {
134    fn from(builtin: Builtin) -> bash::Builtin {
135        let name_str = CString::new(builtin.name).unwrap();
136        let name = name_str.as_ptr() as *mut _;
137        mem::forget(name_str);
138
139        let short_doc_str = CString::new(builtin.usage).unwrap();
140        let short_doc = short_doc_str.as_ptr();
141        mem::forget(short_doc_str);
142
143        let long_docs = iter_to_array!(builtin.help.lines(), str_to_raw);
144        let long_doc = long_docs.as_ptr();
145        mem::forget(long_docs);
146
147        bash::Builtin {
148            name,
149            function: Some(builtin.cfunc),
150            flags: (builtin.flags | Attr::STATIC.bits()) as i32,
151            long_doc,
152            short_doc,
153            handle: ptr::null_mut(),
154        }
155    }
156}
157
158// Enable or disable function overriding for an iterable of builtins.
159pub fn override_funcs<I>(builtins: I, enable: bool) -> crate::Result<()>
160where
161    I: IntoIterator,
162    I::Item: AsRef<str>,
163{
164    for name in builtins {
165        let name = name.as_ref();
166        let builtin_name = CString::new(name).unwrap();
167        let builtin_ptr = builtin_name.as_ptr() as *mut _;
168        match unsafe { bash::builtin_address_internal(builtin_ptr, 1).as_mut() } {
169            Some(b) => {
170                if enable {
171                    b.flags |= Attr::SPECIAL.bits() as i32;
172                } else {
173                    b.flags &= !Attr::SPECIAL.bits() as i32;
174                }
175            }
176            None => {
177                return Err(Error::Base(format!("failed overriding unknown builtin: {name}")));
178            }
179        }
180    }
181
182    Ok(())
183}
184
185/// Toggle an iterable of builtins, returning those were.
186fn toggle_status<I>(builtins: I, enable: bool) -> crate::Result<Vec<String>>
187where
188    I: IntoIterator,
189    I::Item: AsRef<str>,
190{
191    let mut toggled = vec![];
192
193    for name in builtins {
194        let name = name.as_ref();
195        let builtin_name = CString::new(name).unwrap();
196        let builtin_ptr = builtin_name.as_ptr() as *mut _;
197        match unsafe { bash::builtin_address_internal(builtin_ptr, 1).as_mut() } {
198            Some(b) => {
199                let enabled = (b.flags & Attr::ENABLED.bits() as i32) == 1;
200                if enabled != enable {
201                    if enable {
202                        b.flags |= Attr::ENABLED.bits() as i32;
203                    } else {
204                        b.flags &= !Attr::ENABLED.bits() as i32;
205                    }
206                    toggled.push(name.to_string());
207                }
208            }
209            None => return Err(Error::Base(format!("unknown builtin: {name}"))),
210        }
211    }
212
213    Ok(toggled)
214}
215
216/// Disable a given list of builtins by name.
217pub fn disable<I>(builtins: I) -> crate::Result<Vec<String>>
218where
219    I: IntoIterator,
220    I::Item: AsRef<str>,
221{
222    toggle_status(builtins, false)
223}
224
225/// Enable a given list of builtins by name.
226pub fn enable<I>(builtins: I) -> crate::Result<Vec<String>>
227where
228    I: IntoIterator,
229    I::Item: AsRef<str>,
230{
231    toggle_status(builtins, true)
232}
233
234/// Get the sets of enabled and disabled shell builtins.
235pub fn shell_builtins() -> (HashSet<String>, HashSet<String>) {
236    let mut enabled = HashSet::new();
237    let mut disabled = HashSet::new();
238    unsafe {
239        let end = (bash::NUM_SHELL_BUILTINS - 1) as isize;
240        for i in 0..end {
241            let builtin = *bash::SHELL_BUILTINS.offset(i);
242            // builtins with null functions are stubs for reserved keywords
243            if builtin.function.is_some() {
244                let name = String::from(CStr::from_ptr(builtin.name).to_str().unwrap());
245                if (builtin.flags & Attr::ENABLED.bits() as i32) == 1 {
246                    enabled.insert(name);
247                } else {
248                    disabled.insert(name);
249                }
250            }
251        }
252    }
253    (enabled, disabled)
254}
255
256/// Register builtins into the internal list for use.
257pub fn register<I>(builtins: I)
258where
259    I: IntoIterator,
260    I::Item: Into<Builtin>,
261{
262    // convert builtins into pointers
263    let mut builtin_ptrs: Vec<_> = builtins
264        .into_iter()
265        .map(Into::into)
266        .map(|b| Box::into_raw(Box::new(b.into())))
267        .collect();
268
269    unsafe {
270        // add builtins to bash's internal list
271        bash::register_builtins(builtin_ptrs.as_mut_ptr(), builtin_ptrs.len());
272
273        // reclaim pointers for proper deallocation
274        for b in builtin_ptrs {
275            mem::drop(Box::from_raw(b));
276        }
277    }
278}
279
280/// Enable/disable builtins, automatically reverting their status when leaving scope.
281#[derive(Debug, Default)]
282pub struct ScopedBuiltins {
283    names: Vec<String>,
284    enabled: bool,
285}
286
287impl ScopedBuiltins {
288    pub fn disable<I>(values: I) -> crate::Result<Self>
289    where
290        I: IntoIterator,
291        I::Item: AsRef<str>,
292    {
293        Ok(Self {
294            names: disable(values)?,
295            enabled: false,
296        })
297    }
298
299    pub fn enable<I>(values: I) -> crate::Result<Self>
300    where
301        I: IntoIterator,
302        I::Item: AsRef<str>,
303    {
304        Ok(Self {
305            names: enable(values)?,
306            enabled: true,
307        })
308    }
309}
310
311impl Drop for ScopedBuiltins {
312    fn drop(&mut self) {
313        if self.enabled {
314            disable(&self.names).unwrap_or_else(|_| panic!("failed disabling builtins"));
315        } else {
316            enable(&self.names).unwrap_or_else(|_| panic!("failed enabling builtins"));
317        }
318    }
319}
320
321/// Toggle shell options, automatically reverting their status when leaving scope.
322#[derive(Debug, Default)]
323pub struct ScopedOptions {
324    shopt_enabled: Vec<String>,
325    shopt_disabled: Vec<String>,
326    set_enabled: Vec<String>,
327    set_disabled: Vec<String>,
328}
329
330impl ScopedOptions {
331    /// Enable shell options.
332    pub fn enable<'a, I>(&mut self, options: I) -> crate::Result<()>
333    where
334        I: IntoIterator<Item = &'a str>,
335    {
336        let enabled_shopt = bash::shopt_opts();
337        let enabled_set = bash::set_opts();
338
339        for opt in options {
340            if bash::SET_OPTS.contains(opt) {
341                if !enabled_set.contains(opt) {
342                    set::enable(&[opt])?;
343                    self.set_enabled.push(opt.into());
344                }
345            } else if bash::SHOPT_OPTS.contains(opt) {
346                if !enabled_shopt.contains(opt) {
347                    shopt::enable(&[opt])?;
348                    self.shopt_enabled.push(opt.into());
349                }
350            } else {
351                return Err(Error::Base(format!("unknown option: {opt}")));
352            }
353        }
354
355        Ok(())
356    }
357
358    /// Disable shell options.
359    pub fn disable<'a, I>(&mut self, options: I) -> crate::Result<()>
360    where
361        I: IntoIterator<Item = &'a str>,
362    {
363        let enabled_shopt = bash::shopt_opts();
364        let enabled_set = bash::set_opts();
365
366        for opt in options {
367            if bash::SET_OPTS.contains(opt) {
368                if enabled_set.contains(opt) {
369                    set::disable(&[opt])?;
370                    self.set_disabled.push(opt.into());
371                }
372            } else if bash::SHOPT_OPTS.contains(opt) {
373                if enabled_shopt.contains(opt) {
374                    shopt::disable(&[opt])?;
375                    self.shopt_disabled.push(opt.into());
376                }
377            } else {
378                return Err(Error::Base(format!("unknown option: {opt}")));
379            }
380        }
381
382        Ok(())
383    }
384}
385
386impl Drop for ScopedOptions {
387    fn drop(&mut self) {
388        if !self.shopt_enabled.is_empty() {
389            shopt::disable(&self.shopt_enabled).expect("failed unsetting shopt options");
390        }
391
392        if !self.shopt_disabled.is_empty() {
393            shopt::enable(&self.shopt_disabled).expect("failed setting shopt options");
394        }
395
396        if !self.set_enabled.is_empty() {
397            set::disable(&self.set_enabled).expect("failed unsetting set options");
398        }
399
400        if !self.set_disabled.is_empty() {
401            set::enable(&self.set_disabled).expect("failed setting set options");
402        }
403    }
404}
405
406/// Handle builtin errors.
407pub fn handle_error<S: AsRef<str>>(cmd: S, err: Error) -> ExecStatus {
408    // command_not_found_handle builtin messages are unprefixed
409    let lineno = shell::executing_line_number();
410    let msg = match cmd.as_ref() {
411        "command_not_found_handle" => format!("line {lineno}: {err}"),
412        s => format!("line {lineno}: {s}: error: {err}"),
413    };
414
415    let bail = matches!(err, Error::Bail(_));
416    // push error message into shared memory so subshell errors can be captured
417    shell::set_shm_error(&msg, bail);
418
419    // exit subshell with status causing the main process to longjmp to the entry point
420    if bail && !shell::in_main() {
421        process::exit(bash::EX_LONGJMP as i32);
422    }
423
424    ExecStatus::from(err)
425}
426
427/// Run a builtin as called from bash.
428fn run(builtin: &Builtin, args: *mut bash::WordList) -> ExecStatus {
429    // convert raw command args into &str
430    let args = args.to_words();
431    let args: Result<Vec<_>, _> = args.into_iter().collect();
432
433    // run command if args are valid utf8
434    let result = match args {
435        Ok(args) => builtin(&args),
436        Err(e) => Err(Error::Base(format!("invalid args: {e}"))),
437    };
438
439    // handle builtin errors extracting the return status
440    result.unwrap_or_else(|e| handle_error(builtin, e))
441}
442
443/// Create C compatible builtin function wrapper converting between rust and C types.
444#[macro_export]
445macro_rules! make_builtin {
446    ($name:expr, $func_name:ident, $func:expr, $long_doc:expr, $usage:expr) => {
447        use std::ffi::c_int;
448
449        use $crate::builtins::Builtin;
450
451        #[unsafe(no_mangle)]
452        extern "C" fn $func_name(args: *mut $crate::bash::WordList) -> c_int {
453            i32::from($crate::builtins::run(&BUILTIN, args))
454        }
455
456        pub static BUILTIN: Builtin = Builtin {
457            name: $name,
458            func: $func,
459            flags: 0,
460            cfunc: $func_name,
461            help: $long_doc,
462            usage: $usage,
463        };
464    };
465}
466pub use make_builtin;
467
468#[cfg(test)]
469mod tests {
470    use crate::{source, variables};
471
472    use super::*;
473
474    #[test]
475    fn toggle_builtins() {
476        // select a builtin to toggle
477        let (enabled, disabled) = shell_builtins();
478        assert!(!enabled.is_empty());
479        let builtin = enabled.iter().next().unwrap();
480        assert!(!disabled.contains(builtin));
481
482        // disable the builtin
483        disable([builtin]).unwrap();
484        let (enabled, disabled) = shell_builtins();
485        assert!(!enabled.contains(builtin));
486        assert!(disabled.contains(builtin));
487
488        // enable the builtin
489        enable([builtin]).unwrap();
490        let (enabled, disabled) = shell_builtins();
491        assert!(enabled.contains(builtin));
492        assert!(!disabled.contains(builtin));
493
494        // unknown builtin
495        assert!(enable(["nonexistent"]).is_err());
496        assert!(disable(["nonexistent"]).is_err());
497    }
498
499    #[test]
500    fn toggle_overrides() {
501        variables::bind_global("VAR", "1", None, None).unwrap();
502
503        // functions override builtins by default
504        source::string("declare() { (( VAR += 1 )); }").unwrap();
505        source::string("declare").unwrap();
506        assert_eq!(variables::optional("VAR").unwrap(), "2");
507
508        // builtins marked as special override functions
509        override_funcs(["declare"], true).unwrap();
510        source::string("declare").unwrap();
511        assert_eq!(variables::optional("VAR").unwrap(), "2");
512
513        // revert to functions overriding builtins
514        override_funcs(["declare"], false).unwrap();
515        source::string("declare").unwrap();
516        assert_eq!(variables::optional("VAR").unwrap(), "3");
517
518        // unknown builtin
519        assert!(override_funcs(["nonexistent"], true).is_err());
520    }
521
522    #[test]
523    fn scoped_builtins() {
524        assert!(source::string("declare").is_ok());
525        let _builtins = ScopedBuiltins::disable(["declare"]).unwrap();
526        assert!(source::string("declare").is_err());
527        let _builtins = ScopedBuiltins::enable(["declare"]).unwrap();
528        assert!(source::string("declare").is_ok());
529    }
530
531    #[test]
532    fn scoped_options() {
533        // invalid options
534        let mut opts = ScopedOptions::default();
535        assert!(opts.enable(["unknown"]).is_err());
536        assert!(opts.disable(["unknown"]).is_err());
537
538        // shopt options
539        let (enable, disable) = ("autocd", "sourcepath");
540        shopt::disable(&[enable]).unwrap();
541        shopt::enable(&[disable]).unwrap();
542
543        assert!(!bash::shopt_opts().contains(enable));
544        assert!(bash::shopt_opts().contains(disable));
545        {
546            let mut opts = ScopedOptions::default();
547            // perform twice to complete branch coverage
548            opts.enable([enable]).unwrap();
549            opts.enable([enable]).unwrap();
550            opts.disable([disable]).unwrap();
551            opts.disable([disable]).unwrap();
552            assert!(bash::shopt_opts().contains(enable));
553            assert!(!bash::shopt_opts().contains(disable));
554        }
555        assert!(!bash::shopt_opts().contains(enable));
556        assert!(bash::shopt_opts().contains(disable));
557
558        // set options
559        let (enable, disable) = ("noglob", "verbose");
560        set::disable(&[enable]).unwrap();
561        set::enable(&[disable]).unwrap();
562
563        assert!(!bash::set_opts().contains(enable));
564        assert!(bash::set_opts().contains(disable));
565        {
566            let mut opts = ScopedOptions::default();
567            // perform twice to complete branch coverage
568            opts.enable([enable]).unwrap();
569            opts.enable([enable]).unwrap();
570            opts.disable([disable]).unwrap();
571            opts.disable([disable]).unwrap();
572            assert!(bash::set_opts().contains(enable));
573            assert!(!bash::set_opts().contains(disable));
574        }
575        assert!(!bash::set_opts().contains(enable));
576        assert!(bash::set_opts().contains(disable));
577    }
578}