Skip to content

Commit

Permalink
Implement more builtins (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
reubeno authored May 17, 2024
1 parent 1277ff0 commit baa7b2e
Show file tree
Hide file tree
Showing 20 changed files with 282 additions and 65 deletions.
14 changes: 14 additions & 0 deletions cli/tests/cases/builtins/builtin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: "Builtins: builtin"
cases:
- name: "builtin with no builtin"
stdin: builtin

- name: "builtin with unknown builtin"
ignore_stderr: true
stdin: builtin not-a-builtin args

- name: "valid builtin"
stdin: builtin echo "Hello world"

- name: "valid builtin with hyphen args"
stdin: builtin echo -e "Hello\nWorld"
5 changes: 5 additions & 0 deletions cli/tests/cases/builtins/export.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ cases:
echo "Exporting with new value..."
export MY_TEST_VAR="changed value"
env | grep MY_TEST_VAR
- name: "Exporting array"
stdin: |
export arr=(a 1 2)
declare -p arr
16 changes: 16 additions & 0 deletions cli/tests/cases/builtins/let.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: "Builtins: let"
cases:
- name: "Basic let usage"
stdin: |
let 0; echo "0 => $?"
let 1; echo "1 => $?"
let 0==0; echo "0==0 => $?"
let 0!=0; echo "0!=0 => $?"
let 1 0; echo "1 0 => $?"
let 0 1; echo "0 1 => $?"
- name: "let with assignment"
stdin: |
let x=10; echo "x=10 => $?; x==${x}"
16 changes: 16 additions & 0 deletions cli/tests/cases/builtins/readonly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: "Builtins: readonly"
cases:
- name: "making var readonly"
stdin: |
my_var="value"
readonly my_var
echo "Invoking declare -p..."
declare -p my_var
- name: "using readonly with value"
stdin: |
readonly my_var="my_value"
echo "Invoking declare -p..."
declare -p my_var
42 changes: 42 additions & 0 deletions shell/src/builtins/builtin_.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use clap::Parser;
use std::io::Write;

use crate::{
builtin::{BuiltinCommand, BuiltinExitCode},
commands,
};

#[derive(Parser)]
pub(crate) struct BuiltiCommand {
builtin_name: Option<String>,

#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}

#[async_trait::async_trait]
impl BuiltinCommand for BuiltiCommand {
async fn execute(
&self,
mut context: crate::context::CommandExecutionContext<'_>,
) -> Result<crate::builtin::BuiltinExitCode, crate::error::Error> {
if let Some(builtin_name) = &self.builtin_name {
if let Some(builtin) = context.shell.builtins.get(builtin_name) {
context.command_name.clone_from(builtin_name);

let args: Vec<commands::CommandArg> = std::iter::once(builtin_name.into())
.chain(self.args.iter().map(|arg| arg.into()))
.collect();

(builtin.execute_func)(context, args)
.await
.map(|res: crate::builtin::BuiltinResult| res.exit_code)
} else {
writeln!(context.stderr(), "{builtin_name}: command not found")?;
Ok(BuiltinExitCode::Custom(1))
}
} else {
Ok(BuiltinExitCode::Success)
}
}
}
58 changes: 40 additions & 18 deletions shell/src/builtins/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ pub(crate) struct DeclareCommand {
declarations: Vec<CommandArg>,
}

#[derive(Clone, Copy)]
enum DeclareVerb {
Declare,
Local,
Readonly,
}

impl BuiltinDeclarationCommand for DeclareCommand {
fn set_declarations(&mut self, declarations: Vec<CommandArg>) {
self.declarations = declarations;
Expand All @@ -92,7 +99,11 @@ impl BuiltinCommand for DeclareCommand {
&self,
mut context: crate::context::CommandExecutionContext<'_>,
) -> Result<crate::builtin::BuiltinExitCode, crate::error::Error> {
let called_as_local = context.command_name == "local";
let verb = match context.command_name.as_str() {
"local" => DeclareVerb::Local,
"readonly" => DeclareVerb::Readonly,
_ => DeclareVerb::Declare,
};

// TODO: implement declare -I
if self.locals_inherit_from_prev_scope {
Expand All @@ -106,24 +117,24 @@ impl BuiltinCommand for DeclareCommand {
let mut result = BuiltinExitCode::Success;
if !self.declarations.is_empty() {
for declaration in &self.declarations {
if self.print {
if !self.try_display_declaration(&mut context, declaration, called_as_local)? {
if self.print && !matches!(verb, DeclareVerb::Readonly) {
if !self.try_display_declaration(&mut context, declaration, verb)? {
result = BuiltinExitCode::Custom(1);
}
} else {
if !self.process_declaration(&mut context, declaration, called_as_local)? {
if !self.process_declaration(&mut context, declaration, verb)? {
result = BuiltinExitCode::Custom(1);
}
}
}
} else {
// Display matching declarations from the variable environment.
if !self.function_names_only && !self.function_names_or_defs_only {
self.display_matching_env_declarations(&mut context, called_as_local)?;
self.display_matching_env_declarations(&mut context, verb)?;
}

// Do the same for functions.
if !called_as_local
if !matches!(verb, DeclareVerb::Local | DeclareVerb::Readonly)
&& (!self.print || self.function_names_only || self.function_names_or_defs_only)
{
self.display_matching_functions(&mut context)?;
Expand All @@ -139,7 +150,7 @@ impl DeclareCommand {
&self,
context: &mut crate::context::CommandExecutionContext<'_>,
declaration: &CommandArg,
called_as_local: bool,
verb: DeclareVerb,
) -> Result<bool, error::Error> {
let name = match declaration {
CommandArg::String(s) => s,
Expand All @@ -149,7 +160,7 @@ impl DeclareCommand {
}
};

let lookup = if called_as_local {
let lookup = if matches!(verb, DeclareVerb::Local) {
EnvironmentLookup::OnlyInCurrentLocal
} else {
EnvironmentLookup::Anywhere
Expand Down Expand Up @@ -198,13 +209,13 @@ impl DeclareCommand {
&self,
context: &mut crate::context::CommandExecutionContext<'_>,
declaration: &CommandArg,
called_as_local: bool,
verb: DeclareVerb,
) -> Result<bool, error::Error> {
let create_var_local =
called_as_local || (context.shell.in_function() && !self.create_global);
let create_var_local = matches!(verb, DeclareVerb::Local)
|| (context.shell.in_function() && !self.create_global);

if self.function_names_or_defs_only || self.function_names_only {
return self.try_display_declaration(context, declaration, called_as_local);
return self.try_display_declaration(context, declaration, verb);
}

// Extract the variable name and the initial value being assigned (if any).
Expand Down Expand Up @@ -238,7 +249,7 @@ impl DeclareCommand {
var.assign(initial_value, assigned_index.is_some())?;
}

self.apply_attributes_after_update(var)?;
self.apply_attributes_after_update(var, verb)?;
} else {
let unset_type = if self.make_indexed_array.is_some() {
ShellValueUnsetType::IndexedArray
Expand All @@ -258,7 +269,7 @@ impl DeclareCommand {
var.assign(initial_value, false)?;
}

self.apply_attributes_after_update(&mut var)?;
self.apply_attributes_after_update(&mut var, verb)?;

let scope = if create_var_local {
EnvironmentScope::Local
Expand Down Expand Up @@ -351,7 +362,7 @@ impl DeclareCommand {
fn display_matching_env_declarations(
&self,
context: &mut crate::context::CommandExecutionContext<'_>,
called_as_local: bool,
verb: DeclareVerb,
) -> Result<(), error::Error> {
//
// Dump all declarations. Use attribute flags to filter which variables are dumped.
Expand All @@ -362,6 +373,11 @@ impl DeclareCommand {
let mut filters: Vec<Box<dyn Fn((&String, &ShellVariable)) -> bool>> =
vec![Box::new(|(_, v)| v.is_enumerable())];

// Add filters depending on verb.
if matches!(verb, DeclareVerb::Readonly) {
filters.push(Box::new(|(_, v)| v.is_readonly()));
}

// Add filters depending on attribute flags.
if let Some(value) = self.make_indexed_array.to_bool() {
filters.push(Box::new(move |(_, v)| {
Expand Down Expand Up @@ -405,7 +421,7 @@ impl DeclareCommand {
filters.push(Box::new(move |(_, v)| v.is_exported() == value));
}

let iter_policy = if called_as_local {
let iter_policy = if matches!(verb, DeclareVerb::Local) {
EnvironmentLookup::OnlyInCurrentLocal
} else {
EnvironmentLookup::Anywhere
Expand Down Expand Up @@ -517,8 +533,14 @@ impl DeclareCommand {
}

#[allow(clippy::unnecessary_wraps)]
fn apply_attributes_after_update(&self, var: &mut ShellVariable) -> Result<(), error::Error> {
if let Some(value) = self.make_readonly.to_bool() {
fn apply_attributes_after_update(
&self,
var: &mut ShellVariable,
verb: DeclareVerb,
) -> Result<(), error::Error> {
if matches!(verb, DeclareVerb::Readonly) {
var.set_readonly();
} else if let Some(value) = self.make_readonly.to_bool() {
if value {
var.set_readonly();
} else {
Expand Down
2 changes: 1 addition & 1 deletion shell/src/builtins/dot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
pub(crate) struct DotCommand {
pub script_path: String,

#[arg(trailing_var_arg = true)]
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub script_args: Vec<String>,
}

Expand Down
3 changes: 1 addition & 2 deletions shell/src/builtins/echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ pub(crate) struct EchoCommand {
no_interpret_backslash_escapes: bool,

/// Command and args.
#[clap(allow_hyphen_values = true)]
#[arg(trailing_var_arg = true)]
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}

Expand Down
2 changes: 1 addition & 1 deletion shell/src/builtins/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub(crate) struct ExecCommand {
exec_as_login: bool,

/// Command and args.
#[arg(trailing_var_arg = true)]
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}

Expand Down
83 changes: 60 additions & 23 deletions shell/src/builtins/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use itertools::Itertools;
use std::io::Write;

use crate::{
builtin::{BuiltinCommand, BuiltinExitCode},
builtin::{BuiltinCommand, BuiltinDeclarationCommand, BuiltinExitCode},
commands,
env::{EnvironmentLookup, EnvironmentScope},
variables,
};
Expand All @@ -19,8 +20,19 @@ pub(crate) struct ExportCommand {
#[arg(short = 'p')]
display_exported_names: bool,

#[arg(name = "name[=value]")]
names: Vec<String>,
//
// Declarations
//
// N.B. These are skipped by clap, but filled in by the BuiltinDeclarationCommand trait.
//
#[clap(skip)]
declarations: Vec<commands::CommandArg>,
}

impl BuiltinDeclarationCommand for ExportCommand {
fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>) {
self.declarations = declarations;
}
}

#[async_trait::async_trait]
Expand All @@ -29,26 +41,51 @@ impl BuiltinCommand for ExportCommand {
&self,
context: crate::context::CommandExecutionContext<'_>,
) -> Result<crate::builtin::BuiltinExitCode, crate::error::Error> {
if !self.names.is_empty() {
for name in &self.names {
// See if we have a name=value pair; if so, then update the variable
// with the provided value and then mark it exported.
if let Some((name, value)) = name.split_once('=') {
context.shell.env.update_or_add(
name,
variables::ShellValueLiteral::Scalar(value.to_owned()),
|var| {
var.export();
Ok(())
},
EnvironmentLookup::Anywhere,
EnvironmentScope::Global,
)?;
} else {
// Try to find the variable already present; if we find it, then mark it
// exported.
if let Some((_, variable)) = context.shell.env.get_mut(name) {
variable.export();
if !self.declarations.is_empty() {
for decl in &self.declarations {
match decl {
commands::CommandArg::String(s) => {
// Try to find the variable already present; if we find it, then mark it
// exported.
if let Some((_, variable)) = context.shell.env.get_mut(s) {
variable.export();
}
}
commands::CommandArg::Assignment(assignment) => {
let name = match &assignment.name {
parser::ast::AssignmentName::VariableName(name) => name,
parser::ast::AssignmentName::ArrayElementName(_, _) => {
writeln!(context.stderr(), "not a valid variable name")?;
return Ok(BuiltinExitCode::InvalidUsage);
}
};

let value = match &assignment.value {
parser::ast::AssignmentValue::Scalar(s) => {
variables::ShellValueLiteral::Scalar(s.flatten())
}
parser::ast::AssignmentValue::Array(a) => {
variables::ShellValueLiteral::Array(variables::ArrayLiteral(
a.iter()
.map(|(k, v)| {
(k.as_ref().map(|k| k.flatten()), v.flatten())
})
.collect(),
))
}
};

// Update the variable with the provided value and then mark it exported.
context.shell.env.update_or_add(
name,
value,
|var| {
var.export();
Ok(())
},
EnvironmentLookup::Anywhere,
EnvironmentScope::Global,
)?;
}
}
}
Expand Down
File renamed without changes.
Loading

0 comments on commit baa7b2e

Please sign in to comment.