diff --git a/yash-builtin/src/return.rs b/yash-builtin/src/return.rs index f97d7b86..f3a8c8bf 100644 --- a/yash-builtin/src/return.rs +++ b/yash-builtin/src/return.rs @@ -30,8 +30,6 @@ //! `return exit_status` makes the shell return from the currently executing //! function or script with the specified exit status. //! -//! It is an error to use the built-in outside a function or script. -//! //! # Options //! //! The **`-n`** (**`--no-return`**) option makes the built-in not actually quit @@ -47,21 +45,22 @@ //! //! The *exit_status* operand will be the exit status of the built-in. //! -//! If the operand is not given: -//! -//! - If the currently executing script is a trap, the exit status will be the -//! value of `$?` before entering the trap. -//! - Otherwise, the exit status will be the current value of `$?`. +//! If the operand is not given, the exit status will be the current exit status +//! (`$?`). If the built-in is invoked in a trap executed in a function or +//! script and the built-in returns from that function or script, the exit +//! status will be the value of `$?` before entering the trap. //! //! # Errors //! //! If the *exit_status* operand is given but not a valid non-negative integer, //! it is a syntax error. In that case, an error message is printed, and the -//! exit status will be 2, but the built-in still quits a function or script. +//! exit status will be 2 ([`ExitStatus::ERROR`]). //! //! This implementation treats an *exit_status* value greater than 2147483647 as //! a syntax error. //! +//! TODO: What if there is no function or script to return from? +//! //! # Portability //! //! POSIX only requires the return built-in to quit a function or dot script. @@ -79,9 +78,19 @@ //! function or dot script, but returns a [`Result`] having a //! [`Divert::Return`]. The caller is responsible for handling the divert value //! and returning from the function or script. +//! +//! - If an operand specifies an exit status, the divert value will contain the +//! specified exit status. The caller should use it as the exit status of the +//! process. +//! - If no operand is given, the divert value will contain no exit status. The +//! built-in's exit status is the current value of `$?`, and the caller should +//! use it as the exit status of the function or script. However, if the +//! built-in is invoked in a trap executed in the function or script, the caller +//! should use the value of `$?` before entering trap. -use std::future::ready; +use crate::common::syntax_error; use std::future::Future; +use std::num::ParseIntError; use std::ops::ControlFlow::Break; use std::pin::Pin; use yash_env::builtin::Result; @@ -89,52 +98,95 @@ use yash_env::semantics::Divert; use yash_env::semantics::ExitStatus; use yash_env::semantics::Field; use yash_env::Env; +use yash_syntax::source::Location; + +async fn operand_parse_error(env: &mut Env, location: &Location, error: ParseIntError) -> Result { + syntax_error(env, &error.to_string(), location).await +} /// Implementation of the return built-in. /// /// See the [module-level documentation](self) for details. -pub fn builtin_main_sync(_env: &mut Env, args: Vec) -> Result { - // TODO Parse arguments correctly +pub async fn builtin_body(env: &mut Env, args: Vec) -> Result { + // TODO: POSIX does not require the return built-in to support XBD Utility + // Syntax Guidelines. That means the built-in does not have to recognize the + // "--" separator. We should reject the separator in the POSIXly-correct + // mode. // TODO Reject returning from an interactive session + let mut i = args.iter().peekable(); - let no_return = matches!(i.peek(), Some(Field { value, .. }) if value == "-n"); - if no_return { - i.next(); - } + + let no_return = i.next_if(|field| field.value == "-n").is_some(); + let exit_status = match i.next() { - Some(field) => field.value.parse().unwrap_or(2), - None => 0, + None => None, + Some(arg) => match arg.value.parse() { + Ok(exit_status) if exit_status >= 0 => Some(ExitStatus(exit_status)), + Ok(_) => return syntax_error(env, "negative exit status", &arg.origin).await, + Err(e) => return operand_parse_error(env, &arg.origin, e).await, + }, }; - let mut result = Result::new(ExitStatus(exit_status)); - if !no_return { - result.set_divert(Break(Divert::Return)); + + // `i` is fused, so it's safe to call next() again + if let Some(arg) = i.next() { + return syntax_error(env, "too many operands", &arg.origin).await; + } + + if no_return { + Result::new(exit_status.unwrap_or(env.exit_status)) + } else { + let mut result = Result::new(env.exit_status); + result.set_divert(Break(Divert::Return(exit_status))); + result } - result } /// Implementation of the return built-in. /// -/// This function calls [`builtin_main_sync`] and wraps the result in a -/// `Future`. +/// This function calls [`builtin_body`] and wraps the result in a `Box`. pub fn builtin_main( env: &mut yash_env::Env, args: Vec, -) -> Pin>> { - Box::pin(ready(builtin_main_sync(env, args))) +) -> Pin + '_>> { + Box::pin(builtin_body(env, args)) } #[cfg(test)] mod tests { use super::*; + use crate::tests::assert_stderr; + use futures_util::FutureExt; + use std::rc::Rc; use yash_env::semantics::ExitStatus; + use yash_env::stack::Frame; + use yash_env::VirtualSystem; + + #[test] + fn return_without_arguments_with_exit_status_0() { + let mut env = Env::new_virtual(); + let actual_result = builtin_body(&mut env, vec![]).now_or_never().unwrap(); + let mut expected_result = Result::default(); + expected_result.set_divert(Break(Divert::Return(None))); + assert_eq!(actual_result, expected_result); + } + + #[test] + fn return_without_arguments_with_non_zero_exit_status() { + let mut env = Env::new_virtual(); + env.exit_status = ExitStatus(42); + let actual_result = builtin_body(&mut env, vec![]).now_or_never().unwrap(); + let mut expected_result = Result::new(ExitStatus(42)); + expected_result.set_divert(Break(Divert::Return(None))); + assert_eq!(actual_result, expected_result); + } #[test] fn returns_exit_status_specified_without_n_option() { let mut env = Env::new_virtual(); let args = Field::dummies(["42"]); - let actual_result = builtin_main_sync(&mut env, args); - let mut expected_result = Result::new(ExitStatus(42)); - expected_result.set_divert(Break(Divert::Return)); + let actual_result = builtin_body(&mut env, args).now_or_never().unwrap(); + let mut expected_result = Result::default(); + expected_result.set_divert(Break(Divert::Return(Some(ExitStatus(42))))); assert_eq!(actual_result, expected_result); } @@ -142,15 +194,111 @@ mod tests { fn returns_exit_status_12_with_n_option() { let mut env = Env::new_virtual(); let args = Field::dummies(["-n", "12"]); - let result = builtin_main_sync(&mut env, args); + let result = builtin_body(&mut env, args).now_or_never().unwrap(); assert_eq!(result, Result::new(ExitStatus(12))); } #[test] fn returns_exit_status_47_with_n_option() { let mut env = Env::new_virtual(); + env.exit_status = ExitStatus(24); let args = Field::dummies(["-n", "47"]); - let result = builtin_main_sync(&mut env, args); + let result = builtin_body(&mut env, args).now_or_never().unwrap(); assert_eq!(result, Result::new(ExitStatus(47))); } + + #[test] + fn returns_previous_exit_status_with_n_option_without_operand() { + let mut env = Env::new_virtual(); + env.exit_status = ExitStatus(24); + let args = Field::dummies(["-n"]); + let result = builtin_body(&mut env, args).now_or_never().unwrap(); + assert_eq!(result, Result::new(ExitStatus(24))); + } + + #[test] + fn return_with_negative_exit_status_operand() { + let system = Box::new(VirtualSystem::new()); + let state = Rc::clone(&system.state); + let mut env = Env::with_system(system); + let mut env = env.push_frame(Frame::Builtin { + name: Field::dummy("return"), + is_special: true, + }); + let args = Field::dummies(["-1"]); + + let actual_result = builtin_body(&mut env, args).now_or_never().unwrap(); + let mut expected_result = Result::new(ExitStatus::ERROR); + expected_result.set_divert(Break(Divert::Interrupt(None))); + assert_eq!(actual_result, expected_result); + assert_stderr(&state, |stderr| { + assert!(stderr.contains("-1"), "stderr = {stderr:?}") + }); + } + + #[test] + fn exit_with_non_integer_exit_status_operand() { + let system = Box::new(VirtualSystem::new()); + let state = Rc::clone(&system.state); + let mut env = Env::with_system(system); + let mut env = env.push_frame(Frame::Builtin { + name: Field::dummy("return"), + is_special: true, + }); + let args = Field::dummies(["foo"]); + + let actual_result = builtin_body(&mut env, args).now_or_never().unwrap(); + let mut expected_result = Result::new(ExitStatus::ERROR); + expected_result.set_divert(Break(Divert::Interrupt(None))); + assert_eq!(actual_result, expected_result); + assert_stderr(&state, |stderr| { + assert!(stderr.contains("foo"), "stderr = {stderr:?}") + }); + } + + #[test] + fn return_with_too_large_exit_status_operand() { + let system = Box::new(VirtualSystem::new()); + let state = Rc::clone(&system.state); + let mut env = Env::with_system(system); + let mut env = env.push_frame(Frame::Builtin { + name: Field::dummy("return"), + is_special: true, + }); + let args = Field::dummies(["999999999999999999999999999999"]); + + let actual_result = builtin_body(&mut env, args).now_or_never().unwrap(); + let mut expected_result = Result::new(ExitStatus::ERROR); + expected_result.set_divert(Break(Divert::Interrupt(None))); + assert_eq!(actual_result, expected_result); + assert_stderr(&state, |stderr| { + assert!( + stderr.contains("999999999999999999999999999999"), + "stderr = {stderr:?}" + ) + }); + } + + #[test] + fn return_with_too_many_operands() { + let system = Box::new(VirtualSystem::new()); + let state = Rc::clone(&system.state); + let mut env = Env::with_system(system); + let mut env = env.push_frame(Frame::Builtin { + name: Field::dummy("return"), + is_special: true, + }); + let args = Field::dummies(["1", "2"]); + + let actual_result = builtin_body(&mut env, args).now_or_never().unwrap(); + let mut expected_result = Result::new(ExitStatus::ERROR); + expected_result.set_divert(Break(Divert::Interrupt(None))); + assert_eq!(actual_result, expected_result); + assert_stderr(&state, |stderr| { + assert!(stderr.contains("too many operands"), "stderr = {stderr:?}") + }); + } + + // TODO return_with_invalid_option + // TODO return used outside a function or script } diff --git a/yash-env/src/semantics.rs b/yash-env/src/semantics.rs index 60d24b5d..c265d73c 100644 --- a/yash-env/src/semantics.rs +++ b/yash-env/src/semantics.rs @@ -189,7 +189,7 @@ pub enum Divert { }, /// Return from the current function or script. - Return, + Return(Option), /// Interrupt the current shell execution environment. /// @@ -221,8 +221,11 @@ impl Divert { pub fn exit_status(&self) -> Option { use Divert::*; match self { - Continue { .. } | Break { .. } | Return => None, - Interrupt(exit_status) | Exit(exit_status) | Abort(exit_status) => *exit_status, + Continue { .. } | Break { .. } => None, + Return(exit_status) + | Interrupt(exit_status) + | Exit(exit_status) + | Abort(exit_status) => *exit_status, } } } @@ -269,7 +272,7 @@ mod tests { fn apply_errexit_to_non_interrupt() { let mut env = Env::new_virtual(); env.options.set(ErrExit, On); - let subject: Result = Break(Divert::Return); + let subject: Result = Break(Divert::Return(None)); let result = apply_errexit(subject, &env); assert_eq!(result, subject); } diff --git a/yash-semantics/src/command.rs b/yash-semantics/src/command.rs index 04f31c36..0da2487f 100644 --- a/yash-semantics/src/command.rs +++ b/yash-semantics/src/command.rs @@ -140,7 +140,7 @@ mod tests { env.builtins.insert("return", return_builtin()); let list: syntax::List = "return -n 1; return 2; return -n 4".parse().unwrap(); let result = list.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(2)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(2))))); + assert_eq!(env.exit_status, ExitStatus(1)); } } diff --git a/yash-semantics/src/command/and_or.rs b/yash-semantics/src/command/and_or.rs index bf809be1..eff710df 100644 --- a/yash-semantics/src/command/and_or.rs +++ b/yash-semantics/src/command/and_or.rs @@ -279,10 +279,11 @@ mod tests { fn diverting_first() { let mut env = Env::new_virtual(); env.builtins.insert("return", return_builtin()); + env.exit_status = ExitStatus(77); let list: AndOrList = "return 97".parse().unwrap(); let result = list.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(97)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(97))))); + assert_eq!(env.exit_status, ExitStatus(77)); } #[test] @@ -291,8 +292,8 @@ mod tests { env.builtins.insert("return", return_builtin()); let list: AndOrList = "return -n 7 || return 0 && X".parse().unwrap(); let result = list.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(0)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(0))))); + assert_eq!(env.exit_status, ExitStatus(7)); } #[test] diff --git a/yash-semantics/src/command/compound_command/case.rs b/yash-semantics/src/command/compound_command/case.rs index deacb27c..e06ae0fe 100644 --- a/yash-semantics/src/command/compound_command/case.rs +++ b/yash-semantics/src/command/compound_command/case.rs @@ -350,8 +350,8 @@ mod tests { .unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(73)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(73))))); + assert_eq!(env.exit_status, ExitStatus::ERROR); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } diff --git a/yash-semantics/src/command/compound_command/for_loop.rs b/yash-semantics/src/command/compound_command/for_loop.rs index 60cdead1..9877f8ba 100644 --- a/yash-semantics/src/command/compound_command/for_loop.rs +++ b/yash-semantics/src/command/compound_command/for_loop.rs @@ -245,11 +245,12 @@ mod tests { let mut env = Env::with_system(Box::new(system)); env.builtins.insert("echo", echo_builtin()); env.builtins.insert("return", return_builtin()); - let command: CompoundCommand = "for i in 1; do return 2; echo X; done".parse().unwrap(); + let command = "for i in 1; do return -n 5; return 2; echo X; done"; + let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(2)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(2))))); + assert_eq!(env.exit_status, ExitStatus(5)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } diff --git a/yash-semantics/src/command/compound_command/if.rs b/yash-semantics/src/command/compound_command/if.rs index 1ff11ddc..d2c2e424 100644 --- a/yash-semantics/src/command/compound_command/if.rs +++ b/yash-semantics/src/command/compound_command/if.rs @@ -186,12 +186,12 @@ mod tests { fn return_from_condition() { let (mut env, state) = fixture(); env.exit_status = ExitStatus(15); - let command = "if return 42; then echo not reached; fi"; + let command = "if return -n 7; return 42; then echo not reached; fi"; let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(42)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(42))))); + assert_eq!(env.exit_status, ExitStatus(7)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } @@ -199,12 +199,12 @@ mod tests { fn return_from_body() { let (mut env, state) = fixture(); env.exit_status = ExitStatus(15); - let command = "if return -n 0; then return 73; fi"; + let command = "if return -n 0; then return -n 9; return 73; fi"; let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(73)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(73))))); + assert_eq!(env.exit_status, ExitStatus(9)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } @@ -217,8 +217,8 @@ mod tests { let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(52)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(52))))); + assert_eq!(env.exit_status, ExitStatus(2)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } @@ -227,12 +227,12 @@ mod tests { let (mut env, state) = fixture(); env.exit_status = ExitStatus(15); let command = "if return -n 2; then echo not reached 1 - elif return -n 0; then return 47; fi"; + elif return -n 0; then return -n 6; return 47; fi"; let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(47)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(47))))); + assert_eq!(env.exit_status, ExitStatus(6)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } @@ -243,8 +243,8 @@ mod tests { let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(17)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(17))))); + assert_eq!(env.exit_status, ExitStatus(13)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } } diff --git a/yash-semantics/src/command/compound_command/while_loop.rs b/yash-semantics/src/command/compound_command/while_loop.rs index 35fd6e94..b7206152 100644 --- a/yash-semantics/src/command/compound_command/while_loop.rs +++ b/yash-semantics/src/command/compound_command/while_loop.rs @@ -166,22 +166,24 @@ mod tests { #[test] fn return_from_while_condition() { let (mut env, state) = fixture(); - let command: CompoundCommand = "while return 36; echo X; do echo Y; done".parse().unwrap(); + let command = "while return -n 1; return 36; echo X; do echo Y; done"; + let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(36)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(36))))); + assert_eq!(env.exit_status, ExitStatus(1)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } #[test] fn return_from_while_body() { let (mut env, state) = fixture(); - let command: CompoundCommand = "while echo A; do return 42; done".parse().unwrap(); + let command = "while echo A; do return -n 2; return 42; done"; + let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(42)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(42))))); + assert_eq!(env.exit_status, ExitStatus(2)); assert_stdout(&state, |stdout| assert_eq!(stdout, "A\n")); } @@ -202,7 +204,7 @@ mod tests { let command: CompoundCommand = "while check; do check; return; done".parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); + assert_eq!(result, Break(Divert::Return(None))); assert_eq!(env.exit_status, ExitStatus::SUCCESS); assert_eq!(env.stack[..], []); } @@ -410,11 +412,12 @@ mod tests { #[test] fn return_from_until_condition() { let (mut env, state) = fixture(); - let command: CompoundCommand = "until return 12; echo X; do echo Y; done".parse().unwrap(); + let command = "until return -n 5; return 12; echo X; do echo Y; done"; + let command: CompoundCommand = command.parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(12)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(12))))); + assert_eq!(env.exit_status, ExitStatus(5)); assert_stdout(&state, |stdout| assert_eq!(stdout, "")); } @@ -424,8 +427,8 @@ mod tests { let command: CompoundCommand = "until return -n 9; do return 35; done".parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(35)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(35))))); + assert_eq!(env.exit_status, ExitStatus(9)); } #[test] @@ -445,7 +448,7 @@ mod tests { let command: CompoundCommand = "until ! check; do check; return; done".parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); + assert_eq!(result, Break(Divert::Return(None))); assert_eq!(env.exit_status, ExitStatus::SUCCESS); assert_eq!(env.stack[..], []); } diff --git a/yash-semantics/src/command/pipeline.rs b/yash-semantics/src/command/pipeline.rs index b09de961..ff7d2d24 100644 --- a/yash-semantics/src/command/pipeline.rs +++ b/yash-semantics/src/command/pipeline.rs @@ -366,10 +366,11 @@ mod tests { fn single_command_pipeline_returns_exit_status_intact_with_divert() { let mut env = Env::new_virtual(); env.builtins.insert("return", return_builtin()); + env.exit_status = ExitStatus(17); let pipeline: syntax::Pipeline = "return 37".parse().unwrap(); let result = pipeline.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(37)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(37))))); + assert_eq!(env.exit_status, ExitStatus(17)); } #[test] @@ -487,10 +488,11 @@ mod tests { fn not_inverting_exit_status_with_divert() { let mut env = Env::new_virtual(); env.builtins.insert("return", return_builtin()); + env.exit_status = ExitStatus(3); let pipeline: syntax::Pipeline = "! return 15".parse().unwrap(); let result = pipeline.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); - assert_eq!(env.exit_status, ExitStatus(15)); + assert_eq!(result, Break(Divert::Return(Some(ExitStatus(15))))); + assert_eq!(env.exit_status, ExitStatus(3)); } #[test] diff --git a/yash-semantics/src/command/simple_command.rs b/yash-semantics/src/command/simple_command.rs index 5d56414b..5a70cab9 100644 --- a/yash-semantics/src/command/simple_command.rs +++ b/yash-semantics/src/command/simple_command.rs @@ -111,6 +111,10 @@ use yash_syntax::syntax::Redir; /// After executing the function body, the contexts are /// [popped](yash_env::variable::VariableSet::pop_context). /// +/// If the execution results in a [`Divert::Return`], it is consumed, and its +/// associated exit status, if any, is set as the exit status of the simple +/// command. +/// /// ## External utility /// /// If the target is an external utility, a subshell is created. Redirections @@ -383,7 +387,10 @@ async fn execute_function( // TODO Update control flow stack let result = function.body.execute(&mut inner).await; - if result == Break(Divert::Return) { + if let Break(Divert::Return(exit_status)) = result { + if let Some(exit_status) = exit_status { + inner.exit_status = exit_status; + } Continue(()) } else { result @@ -683,10 +690,22 @@ mod tests { #[test] fn simple_command_returns_exit_status_from_builtin_with_divert() { let mut env = Env::new_virtual(); - env.builtins.insert("return", return_builtin()); - let command: syntax::SimpleCommand = "return 37".parse().unwrap(); + env.builtins.insert( + "foo", + Builtin { + r#type: yash_env::builtin::Type::Special, + execute: |_env, _args| { + Box::pin(std::future::ready({ + let mut result = yash_env::builtin::Result::new(ExitStatus(37)); + result.set_divert(Break(Divert::Return(None))); + result + })) + }, + }, + ); + let command: syntax::SimpleCommand = "foo".parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); - assert_eq!(result, Break(Divert::Return)); + assert_eq!(result, Break(Divert::Return(None))); assert_eq!(env.exit_status, ExitStatus(37)); } @@ -916,7 +935,25 @@ mod tests { } #[test] - fn function_call_consumes_return() { + fn function_call_consumes_return_without_exit_status() { + use yash_env::function::HashEntry; + let mut env = Env::new_virtual(); + env.builtins.insert("return", return_builtin()); + env.functions.insert(HashEntry(Rc::new(Function { + name: "foo".to_string(), + body: Rc::new("{ return; }".parse().unwrap()), + origin: Location::dummy("dummy"), + is_read_only: false, + }))); + env.exit_status = ExitStatus(17); + let command: syntax::SimpleCommand = "foo".parse().unwrap(); + let result = command.execute(&mut env).now_or_never().unwrap(); + assert_eq!(result, Continue(())); + assert_eq!(env.exit_status, ExitStatus(17)); + } + + #[test] + fn function_call_consumes_return_with_exit_status() { use yash_env::function::HashEntry; let mut env = Env::new_virtual(); env.builtins.insert("return", return_builtin()); diff --git a/yash-semantics/src/lib.rs b/yash-semantics/src/lib.rs index 645a9eb9..c214d1a9 100644 --- a/yash-semantics/src/lib.rs +++ b/yash-semantics/src/lib.rs @@ -54,7 +54,7 @@ pub(crate) mod tests { use std::future::pending; use std::future::ready; use std::future::Future; - use std::ops::ControlFlow::{Break, Continue}; + use std::ops::ControlFlow::Break; use std::pin::Pin; use std::rc::Rc; use std::str::from_utf8; @@ -176,22 +176,19 @@ pub(crate) mod tests { } fn return_builtin_main( - _env: &mut Env, - mut args: Vec, + env: &mut Env, + args: Vec, ) -> Pin>> { - let divert = match args.get(0) { - Some(field) if field.value == "-n" => { - args.remove(0); - Continue(()) - } - _ => Break(Divert::Return), - }; - let exit_status = match args.get(0) { - Some(field) => field.value.parse().unwrap_or(2), - None => 0, + let mut i = args.iter().peekable(); + let no_return = i.next_if(|field| field.value == "-n").is_some(); + let exit_status = i.next().map(|arg| ExitStatus(arg.value.parse().unwrap())); + let result = if no_return { + yash_env::builtin::Result::new(exit_status.unwrap_or(env.exit_status)) + } else { + let mut result = yash_env::builtin::Result::new(env.exit_status); + result.set_divert(Break(Divert::Return(exit_status))); + result }; - let mut result = yash_env::builtin::Result::new(ExitStatus(exit_status)); - result.set_divert(divert); Box::pin(ready(result)) } diff --git a/yash-semantics/src/trap/exit.rs b/yash-semantics/src/trap/exit.rs index 30d4647e..70bf6c16 100644 --- a/yash-semantics/src/trap/exit.rs +++ b/yash-semantics/src/trap/exit.rs @@ -170,7 +170,7 @@ mod tests { .set_action( &mut env.system, Condition::Exit, - Action::Command("return 53".into()), + Action::Command("return -n 53; return".into()), Location::dummy(""), false, ) diff --git a/yash/src/lib.rs b/yash/src/lib.rs index ea17547f..57e06876 100644 --- a/yash/src/lib.rs +++ b/yash/src/lib.rs @@ -83,7 +83,7 @@ async fn parse_and_print(mut env: yash_env::Env) -> i32 { Continue(()) | Break(Divert::Continue { .. }) | Break(Divert::Break { .. }) - | Break(Divert::Return) + | Break(Divert::Return(_)) | Break(Divert::Interrupt(_)) | Break(Divert::Exit(_)) => run_exit_trap(&mut env).await, Break(Divert::Abort(_)) => (), diff --git a/yash/tests/builtin/mod.rs b/yash/tests/builtin/mod.rs index 4abcc6ad..b9f0e595 100644 --- a/yash/tests/builtin/mod.rs +++ b/yash/tests/builtin/mod.rs @@ -15,3 +15,4 @@ // along with this program. If not, see . mod exit; +mod r#return; diff --git a/yash/tests/builtin/return.rs b/yash/tests/builtin/return.rs new file mode 100644 index 00000000..470b4c64 --- /dev/null +++ b/yash/tests/builtin/return.rs @@ -0,0 +1,91 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2023 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{file_with_content, subject}; +use std::str::from_utf8; + +#[test] +fn exiting_from_function_without_exit_status() { + let stdin = file_with_content(b"f() { (exit 47); return; echo X; }\nf\n"); + let result = subject().stdin(stdin).output().unwrap(); + assert_eq!(result.status.code(), Some(47), "{:?}", result.status); + assert_eq!(from_utf8(&result.stdout), Ok("")); + assert_eq!(from_utf8(&result.stderr), Ok("")); +} + +#[test] +fn exiting_from_function_with_exit_status() { + let stdin = file_with_content(b"f() { return 21; echo X; }\nf\n"); + let result = subject().stdin(stdin).output().unwrap(); + assert_eq!(result.status.code(), Some(21), "{:?}", result.status); + assert_eq!(from_utf8(&result.stdout), Ok("")); + assert_eq!(from_utf8(&result.stderr), Ok("")); +} + +#[test] +fn exiting_from_nested_function() { + let stdin = file_with_content(b"f() { return 93; echo X $?; }\ng() { f; echo Y $?; }\ng\n"); + let result = subject().stdin(stdin).output().unwrap(); + assert_eq!(result.status.code(), Some(0), "{:?}", result.status); + assert_eq!(from_utf8(&result.stdout), Ok("Y 93\n")); + assert_eq!(from_utf8(&result.stderr), Ok("")); +} + +#[test] +fn exiting_from_signal_trap_running_function_without_exit_status() { + let stdin = file_with_content( + b"trap '(exit 1); return; echo X $?' INT + f() { (kill -INT $$; exit 2); echo Y $?; } + f + echo Z $?\n", + ); + let result = subject().stdin(stdin).output().unwrap(); + assert_eq!(result.status.code(), Some(0), "{:?}", result.status); + assert_eq!(from_utf8(&result.stdout), Ok("Z 2\n")); + assert_eq!(from_utf8(&result.stderr), Ok("")); +} + +#[test] +fn exiting_from_signal_trap_running_function_with_exit_status() { + let stdin = file_with_content( + b"trap '(exit 1); return 10; echo X $?' INT + f() { (kill -INT $$; exit 2); echo Y $?; } + f + echo Z $?\n", + ); + let result = subject().stdin(stdin).output().unwrap(); + assert_eq!(result.status.code(), Some(0), "{:?}", result.status); + assert_eq!(from_utf8(&result.stdout), Ok("Z 10\n")); + assert_eq!(from_utf8(&result.stderr), Ok("")); +} + +#[test] +fn exiting_from_exit_trap_running_function_without_exit_status() { + let stdin = file_with_content(b"trap '(exit 1); return; echo X $?' EXIT\nexit 2\n"); + let result = subject().stdin(stdin).output().unwrap(); + assert_eq!(result.status.code(), Some(2), "{:?}", result.status); + assert_eq!(from_utf8(&result.stdout), Ok("")); + assert_eq!(from_utf8(&result.stderr), Ok("")); +} + +#[test] +fn exiting_from_exit_trap_running_function_with_exit_status() { + let stdin = file_with_content(b"trap '(exit 1); return 7; echo X $?' EXIT\nexit 3\n"); + let result = subject().stdin(stdin).output().unwrap(); + assert_eq!(result.status.code(), Some(7), "{:?}", result.status); + assert_eq!(from_utf8(&result.stdout), Ok("")); + assert_eq!(from_utf8(&result.stderr), Ok("")); +}