Crate bash_builtins

source ·
Expand description

This crate provides utilities to implement loadable builtins for bash. It reuses functions provided by bash as much as possible in order to keep compatibility with existing builtins.

§What are Loadable Builtins

Bash, like most shells, has builtins. A builtin looks like a regular command, but it is executed in the shell process. Some builtins are used to interact with the shell (like cd or jobs), and others are common utilities (like printf or test).

New builtins can be created in a running shell as loadable builtins, using code from a dynamic library (for example, a .so file in Linux). This is done with the enable -f command.

For example, if the crate name is foo, and it defines a bar builtin, the following commands will load it:

$ cargo build --release

$ enable -f target/release/libfoo.so bar

§Usage

§Crate Configuration

The crate where the builtin is implemented has to include cdylib in its crate-type field. This is required to build a dynamic library.

Cargo.toml should contain something similar to this:

[dependencies]
bash-builtins = "0.4.1"

[lib]
crate-type = [ "cdylib" ]

§Main Items

These are the main items to implement a builtin:

  • The builtin_metadata!() macro, to generate functions and declarations required by bash.

  • The BuiltinOptions derive macro, to generate an option parser.

  • The Builtin trait, to provide the builtin functionality.

  • The Args type, to access to the command-line arguments.

A single crate can contain multiple builtins. Each builtin requires its own call to builtin_metadata!().

§Basic Structure

use bash_builtins::{builtin_metadata, Args, Builtin, BuiltinOptions, Result};

builtin_metadata!(
    // Builtin metadata.
);

struct SomeName {
    // Fields to store state.
}

#[derive(BuiltinOptions)]
enum Opt {
    // Options from the command-line arguments.
}

impl Builtin for SomeName {
    fn call(&mut self, args: &mut Args) -> Result<()> {
        // builtin implementation
        Ok(())
    }
}

§Example

The following example is a simple counter.

It accepts some options to modify the stored value.

//! Bash builtin to implement a counter.

use bash_builtins::{builtin_metadata, Args, Builtin, BuiltinOptions, Result};
use std::io::{stdout, Write};

builtin_metadata!(
    name = "counter",
    create = Counter::default,
    short_doc = "counter [-r] [-s value] [-a value]",
    long_doc = "
        Print a value, and increment it.

        Options:
          -r\tReset the value to 0.
          -s\tSet the counter to a specific value.
          -a\tIncrement the counter by a value.
    ",
);

#[derive(BuiltinOptions)]
enum Opt {
    #[opt = 'r']
    Reset,

    #[opt = 's']
    Set(isize),

    #[opt = 'a']
    Add(isize),
}

#[derive(Default)]
struct Counter(isize);

impl Builtin for Counter {
    fn call(&mut self, args: &mut Args) -> Result<()> {
        // No options: print the current value and increment it.
        if args.is_empty() {
            // Use writeln!() instead of println!() to avoid
            // panicking if stdout is closed.
            writeln!(stdout(), "{}", self.0)?;

            self.0 += 1;
            return Ok(());
        }

        // Parse options. They can change the value of the counter, but the
        // updated value is stored only if we don't get any error.
        let mut value = self.0;
        for opt in args.options() {
            match opt? {
                Opt::Reset => value = 0,
                Opt::Set(v) => value = v,
                Opt::Add(v) => value += v,
            }
        }

        // It is an error if we receive free arguments.
        args.finished()?;

        // Update the state and exit.
        self.0 = value;
        Ok(())
    }
}

This example is available in the examples/counter.rs file of the Git repository of this crate.

It can be tested with the following commands:

$ cargo build --release --examples

$ enable -f target/release/examples/libcounter.so counter

$ counter
0

$ counter
1

$ help counter
counter: counter [-r] [-s value] [-a value]
    Print a value, and increment it.

    Options:
      -r        Reset the value to 0.
      -s        Set the counter to a specific value.
      -a        Increment the counter by a value.

$ counter -s -100

$ counter
-100

$ counter abcd
bash: counter: too many arguments

$ enable -d counter

$ counter
bash: counter: command not found

§Builtin Documentation

A bash builtin has two fields for the documentation:

  • short_doc: a single line of text to describe how to use the builtin.
  • long_doc: a detailed explanation of the builtin.

Both fields are optional, but it is recommend to include them.

See the documentation of the builtin_metadata!() macro for more details.

§Builtin Initialization

When the builtin is loaded, the function given in either create or try_create is executed. This function will create a new instance of a type that implements the Builtin trait.

try_create is used if the initialization mail fails.

§Example of a Fallible Initialization

use std::fs::File;

builtin_metadata!(
    // …
    try_create = Foo::new,
);

struct Foo {
    file: File
}

impl Foo {
    fn new() -> Result<Foo> {
        let file = File::open("/some/config/file")?;
        Ok(Foo { file })
    }
}

impl Builtin for Foo {
    fn call(&mut self, args: &mut Args) -> Result<()> {
        // …
        Ok(())
    }
}

§Builtin Removal

A loadable builtin can be removed from a running shell with enable -d.

If a builtin needs to run any cleanup process when it is unloaded, then it must implement Drop. The value is dropped just before the builtin is deleted.

§Parsing Command Line Options

Bash builtins use an internal implementation of getopt() to parse command line arguments. The BuiltinOptions derive macro provides an easy-to-use method to generate an options parser on top of this getopt().

See the macro documentation for details on how to use it.

§Error Handling

The macros error!() and warning!() can be used to produce log messages to the standard error stream (stderr). They use the bash functions builtin_error and builtin_warning.

Recoverable errors can be used as the return value of Builtin::call, usually with the ? operator. In such cases, the message from the error is printed to stderr, and the exit code of the builtin is 1.

Use Error::ExitCode to return a specific exit code. See the Error documentation for more details.

§Using Shell Variables

The module variables contains functions to manage the shell variables.

§Example

The following example uses the variable $SOMENAME_LIMIT to set the configuration value for the builtin. If it is not present, or its value is not a valid usize, it uses a default value

use bash_builtins::variables;

const DEFAULT_LIMIT: usize = 1024;

const LIMIT_VAR_NAME: &str = "SOMENAME_LIMIT";

fn get_limit() -> usize {
    variables::find_as_string(LIMIT_VAR_NAME)
        .as_ref()
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.parse().ok())
        .unwrap_or(DEFAULT_LIMIT)
}

§Creating Dynamic Variables

Dynamic variables are shell variables that use custom functions each time they are accessed (like $SECONDS or $RANDOM).

Use variables::bind to create a dynamic variable with any type implementing DynamicVariable.

§Panic Handling

Panics are captured with panic::catch_unwind, so they should not reach the bash process.

After a panic the builtin is “poisoned”, and any attempt to use it will print the error invalid internal state on the terminal. Users will have to remove it (enable -d) and enable it again. Also, when a poisoned builtin is removed, its destructors (if any) are not executed.

If you want to avoid this behaviour you have to use panic::catch_unwind in your own code.

It is important to not set the panic setting to "abort". If the dynamic library is built with this setting, a panic will terminate the bash process.

Re-exports§

  • pub use args::BuiltinOptions;

Modules§

  • Traits for conversions between types.
  • Functions to write log messages.
  • This module contains functions to get, set, or unset shell variables.

Macros§

Structs§

  • This structure provides access to the command-line arguments passed to the builtin.

Enums§

Traits§

  • The Builtin trait contains the implementation for a bash builtin.

Type Aliases§

Derive Macros§

  • A derive macro to generate a command-line arguments parser for Args::options. The parser uses the getopt() implementation provided by bash.