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§
- Macro to generate the code to define a new builtin for bash.
Structs§
- This structure provides access to the command-line arguments passed to the builtin.
Enums§
- The error type for
Builtin::call
.
Traits§
- The
Builtin
trait contains the implementation for a bash builtin.
Type Aliases§
- A specialized
Result
type for this crate.
Derive Macros§
- A derive macro to generate a command-line arguments parser for
Args::options
. The parser uses thegetopt()
implementation provided by bash.