use async_trait::async_trait;
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::{ExecContext, ParamSchema, Tool, ToolArgs, ToolSchema};
pub struct Set;
#[async_trait]
impl Tool for Set {
fn name(&self) -> &str {
"set"
}
fn schema(&self) -> ToolSchema {
ToolSchema::new("set", "Set shell options")
.param(ParamSchema::optional(
"options",
"string",
Value::Null,
"Shell options (-e, +e, -o latch, -o trash, -o glob, etc.)",
))
.example("Exit on error", "set -e")
.example("Disable exit on error", "set +e")
.example("Enable confirmation latch", "set -o latch")
.example("Enable trash-on-delete", "set -o trash")
.example("Disable glob expansion", "set +o glob")
}
async fn execute(&self, args: ToolArgs, ctx: &mut ExecContext) -> ExecResult {
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.latch_enabled() {
output.push_str("set -o latch\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 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) {
match name {
"latch" => ctx.scope.set_latch_enabled(true),
"trash" => ctx.scope.set_trash_enabled(true),
"glob" => ctx.scope.set_glob_enabled(true),
_ => {
if name == "output-limit" || name.starts_with("output-limit=") {
if let Some(size_str) = name.strip_prefix("output-limit=") {
if let Ok(bytes) = crate::output_limit::parse_size(size_str) {
ctx.output_limit.set_limit(Some(bytes));
}
} else if ctx.output_limit.max_bytes().is_none() {
ctx.output_limit.set_limit(Some(crate::output_limit::OutputLimitConfig::default_limit()));
}
}
}
}
i += 1; }
}
"+o" => {
if let Some(&name) = positionals.get(i + 1) {
match name {
"latch" => ctx.scope.set_latch_enabled(false),
"trash" => ctx.scope.set_trash_enabled(false),
"glob" => ctx.scope.set_glob_enabled(false),
"output-limit" => ctx.output_limit.set_limit(None),
_ => {}
}
i += 1;
}
}
_ => {} }
i += 1;
}
if args.flags.contains("o")
&& !positionals.iter().any(|p| *p == "-o" || *p == "+o")
{
for &name in &positionals {
match name {
"latch" => { ctx.scope.set_latch_enabled(true); break; }
"trash" => { ctx.scope.set_trash_enabled(true); break; }
"glob" => { ctx.scope.set_glob_enabled(true); break; }
_ => {
if name == "output-limit" || name.starts_with("output-limit=") {
if let Some(size_str) = name.strip_prefix("output-limit=") {
if let Ok(bytes) = crate::output_limit::parse_size(size_str) {
ctx.output_limit.set_limit(Some(bytes));
}
} else if ctx.output_limit.max_bytes().is_none() {
ctx.output_limit.set_limit(Some(crate::output_limit::OutputLimitConfig::default_limit()));
}
break;
}
}
}
}
}
ExecResult::success("")
}
}
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_options() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("-u".into()));
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("pipefail".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_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() {
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());
assert!(ctx.scope.error_exit_enabled());
}
#[tokio::test]
async fn test_set_o_latch_enables() {
let mut ctx = make_ctx();
assert!(!ctx.scope.latch_enabled());
let mut args = ToolArgs::new();
args.positional.push(Value::String("-o".into()));
args.positional.push(Value::String("latch".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.latch_enabled());
}
#[tokio::test]
async fn test_set_plus_o_latch_disables() {
let mut ctx = make_ctx();
ctx.scope.set_latch_enabled(true);
let mut args = ToolArgs::new();
args.positional.push(Value::String("+o".into()));
args.positional.push(Value::String("latch".into()));
let result = Set.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(!ctx.scope.latch_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_latch_enabled(true);
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 latch"));
assert!(result.text_out().contains("set -o trash"));
}
#[tokio::test]
async fn test_set_o_unknown_ignored() {
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());
}
#[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");
}
}