use async_trait::async_trait;
use clap::{CommandFactory, Parser};
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData, OutputNode};
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
pub struct Set;
const VALID_SET_O_NAMES: &[&str] = &["glob", "output-limit[=SIZE]", "pipefail", "trash"];
fn apply_set_o(ctx: &mut ExecContext, name: &str, enable: bool) -> Result<(), String> {
let sigil = if enable { '-' } else { '+' };
match name {
"trash" => ctx.scope.set_trash_enabled(enable),
"glob" => ctx.scope.set_glob_enabled(enable),
"pipefail" => ctx.scope.set_pipefail_enabled(enable),
"output-limit" => {
if enable {
if ctx.output_limit.max_bytes().is_none() {
ctx.output_limit.set_limit(Some(
crate::output_limit::OutputLimitConfig::default_limit(),
));
}
} else {
ctx.output_limit.set_limit(None);
}
}
_ if enable && name.starts_with("output-limit=") => {
let size_str = &name["output-limit=".len()..];
let bytes = crate::output_limit::parse_size(size_str)
.map_err(|e| format!("set: {sigil}o output-limit={size_str}: {e}"))?;
ctx.output_limit.set_limit(Some(bytes));
}
_ => {
return Err(format!(
"set: {sigil}o {name}: unknown option — valid names are {}",
VALID_SET_O_NAMES.join(", ")
));
}
}
Ok(())
}
#[derive(Parser, Debug)]
#[command(name = "set", about = "Set shell options")]
struct SetArgs {
#[command(flatten)]
global: GlobalFlags,
options: Vec<String>,
}
#[async_trait]
impl Tool for Set {
fn name(&self) -> &str {
"set"
}
fn schema(&self) -> ToolSchema {
schema_from_clap(
&SetArgs::command(),
"set",
"Set shell options",
[
("Exit on error", "set -e"),
("Disable exit on error", "set +e"),
("Enable trash-on-delete", "set -o trash"),
("Disable glob expansion", "set +o glob"),
],
)
}
async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let mut clap_argv: Vec<String> = Vec::new();
if args.flags.contains("json") {
clap_argv.push("--json".to_string());
}
let parsed = match SetArgs::try_parse_from(
std::iter::once("set".to_string()).chain(clap_argv),
) {
Ok(p) => p,
Err(e) => return ExecResult::failure(2, format!("set: {e}")),
};
parsed.global.apply(ctx);
if args.positional.is_empty() && args.flags.is_empty() {
let mut output = String::new();
if ctx.scope.error_exit_enabled() {
output.push_str("set -e\n");
}
if ctx.scope.trash_enabled() {
output.push_str("set -o trash\n");
}
if !ctx.scope.glob_enabled() {
output.push_str("set +o glob\n");
}
if let Some(bytes) = ctx.output_limit.max_bytes() {
output.push_str(&format!("set -o output-limit={}\n", format_size_for_set(bytes)));
}
return ExecResult::with_output(OutputData::text(output.trim_end()));
}
for flag in &args.flags {
match flag.as_str() {
"e" => ctx.scope.set_error_exit(true),
"o" => {} _ => {}
}
}
let positionals: Vec<&str> = args
.positional
.iter()
.filter_map(|v| match v {
Value::String(s) => Some(s.as_str()),
_ => None,
})
.collect();
let bare_dash_o = (args.flags.contains("o") && positionals.is_empty())
|| positionals.last().is_some_and(|p| *p == "-o");
if bare_dash_o {
return ExecResult::with_output(OutputData::table(
vec!["OPTION".to_string(), "STATE".to_string()],
vec![
option_row("errexit", ctx.scope.error_exit_enabled()),
option_row("glob", ctx.scope.glob_enabled()),
option_row("pipefail", ctx.scope.pipefail_enabled()),
option_row("output-limit", ctx.output_limit.max_bytes().is_some()),
option_row("trash", ctx.scope.trash_enabled()),
],
));
}
let mut i = 0;
while i < positionals.len() {
let opt = positionals[i];
match opt {
"-e" => ctx.scope.set_error_exit(true),
"+e" => ctx.scope.set_error_exit(false),
"-o" => {
if let Some(&name) = positionals.get(i + 1) {
if let Err(msg) = apply_set_o(ctx, name, true) {
return ExecResult::failure(1, msg);
}
i += 1; }
}
"+o" => {
if let Some(&name) = positionals.get(i + 1) {
if let Err(msg) = apply_set_o(ctx, name, false) {
return ExecResult::failure(1, msg);
}
i += 1;
}
}
_ => {}
}
i += 1;
}
if args.flags.contains("o")
&& !positionals.iter().any(|p| *p == "-o" || *p == "+o")
{
if let Some(&name) = positionals.first() {
if let Err(msg) = apply_set_o(ctx, name, true) {
return ExecResult::failure(1, msg);
}
}
}
ExecResult::success("")
}
}
fn option_row(name: &str, enabled: bool) -> OutputNode {
OutputNode::new(name).with_cells(vec![if enabled { "on" } else { "off" }.to_string()])
}
fn format_size_for_set(bytes: usize) -> String {
if bytes % (1024 * 1024) == 0 {
format!("{}M", bytes / (1024 * 1024))
} else if bytes % 1024 == 0 {
format!("{}K", bytes / 1024)
} else {
bytes.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;
fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
vfs.mount("/", MemoryFs::new());
ExecContext::new(Arc::new(vfs))
}
#[tokio::test]
async fn test_set_e_enables_error_exit() {
let mut ctx = make_ctx();
assert!(!ctx.scope.error_exit_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("-e".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.error_exit_enabled());
}
#[tokio::test]
async fn test_set_plus_e_disables_error_exit() {
let mut ctx = make_ctx();
ctx.scope.set_error_exit(true);
assert!(ctx.scope.error_exit_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("+e".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(!ctx.scope.error_exit_enabled());
}
#[tokio::test]
async fn test_set_ignores_unknown_bare_flags() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("-u".into()));
args.positional.push(Value::String("-x".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
}
#[tokio::test]
async fn test_set_o_pipefail_enables_it() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("pipefail".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok(), "err={}", result.err);
assert!(ctx.scope.pipefail_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("+o".into()));
args.positional.push(Value::String("pipefail".into()));
assert!(Set.execute(args, &mut ctx).await.ok());
assert!(!ctx.scope.pipefail_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("nosuchoption".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(!result.ok(), "an unknown -o name must still be refused");
}
#[tokio::test]
async fn test_set_no_args_shows_settings() {
let mut ctx = make_ctx();
ctx.scope.set_error_exit(true);
let args = ToolArgs::new();
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.text_out().contains("set -e"));
}
#[tokio::test]
async fn test_set_euo_pipefail_sets_all_of_them() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("-e".into()));
args.positional.push(Value::String("-u".into()));
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("pipefail".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok(), "err={}", result.err);
assert!(ctx.scope.error_exit_enabled());
assert!(ctx.scope.pipefail_enabled());
}
#[tokio::test]
async fn the_flag_split_parse_path_still_enables() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("o".to_string());
args.positional.push(Value::String("trash".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.trash_enabled());
}
#[tokio::test]
async fn test_set_o_trash_enables() {
let mut ctx = make_ctx();
assert!(!ctx.scope.trash_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("trash".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.trash_enabled());
}
#[tokio::test]
async fn test_set_plus_o_trash_disables() {
let mut ctx = make_ctx();
ctx.scope.set_trash_enabled(true);
let mut args = ToolArgs::new();
args.positional.push(Value::String("+o".into()));
args.positional.push(Value::String("trash".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(!ctx.scope.trash_enabled());
}
#[tokio::test]
async fn test_set_no_args_shows_all_options() {
let mut ctx = make_ctx();
ctx.scope.set_trash_enabled(true);
let args = ToolArgs::new();
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.text_out().contains("set -o trash"));
}
#[tokio::test]
async fn test_set_o_unknown_name_fails() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("bogusname".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(!result.ok());
assert!(result.err.contains("bogusname"));
assert!(
result.err.contains("glob") && result.err.contains("trash") && result.err.contains("output-limit"),
"error should name the valid set: {:?}",
result.err
);
}
#[tokio::test]
async fn test_set_o_output_limit_enables_default() {
let mut ctx = make_ctx();
assert!(!ctx.output_limit.is_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("output-limit".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.output_limit.is_enabled());
assert_eq!(ctx.output_limit.max_bytes(), Some(crate::output_limit::OutputLimitConfig::default_limit()));
}
#[tokio::test]
async fn test_set_o_output_limit_with_size() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("output-limit=16K".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(ctx.output_limit.max_bytes(), Some(16 * 1024));
}
#[tokio::test]
async fn test_set_plus_o_output_limit_disables() {
let mut ctx = make_ctx();
ctx.output_limit.set_limit(Some(8 * 1024));
assert!(ctx.output_limit.is_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("+o".into()));
args.positional.push(Value::String("output-limit".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(!ctx.output_limit.is_enabled());
}
#[tokio::test]
async fn test_set_no_args_shows_output_limit() {
let mut ctx = make_ctx();
ctx.output_limit.set_limit(Some(4 * 1024));
let args = ToolArgs::new();
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.text_out().contains("set -o output-limit=4K"));
}
#[tokio::test]
async fn test_set_no_args_hides_output_limit_when_disabled() {
let mut ctx = make_ctx();
let args = ToolArgs::new();
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(!result.text_out().contains("output-limit"));
}
#[test]
fn test_format_size_for_set() {
assert_eq!(format_size_for_set(1024), "1K");
assert_eq!(format_size_for_set(8 * 1024), "8K");
assert_eq!(format_size_for_set(1024 * 1024), "1M");
assert_eq!(format_size_for_set(512), "512");
}
}