Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement more builtins #15

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading