From b54c9617ef73c56c3f97e8e2cacf9ddc45a2a0c4 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Mon, 20 Nov 2023 00:46:54 +0900 Subject: [PATCH 1/6] Basic export built-in implementation --- yash-builtin/src/export.rs | 115 ++++++++++++++++++++++++++++++++++++ yash-builtin/src/lib.rs | 8 +++ yash-builtin/src/typeset.rs | 5 +- 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 yash-builtin/src/export.rs diff --git a/yash-builtin/src/export.rs b/yash-builtin/src/export.rs new file mode 100644 index 00000000..07ffbe80 --- /dev/null +++ b/yash-builtin/src/export.rs @@ -0,0 +1,115 @@ +// 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 . + +//! Export built-in +//! +//! The **`export`** built-in exports shell variables to the environment. +//! +//! # Synopsis +//! +//! ```sh +//! export [-p] [name[=value]…] +//! ``` +//! +//! # Description +//! +//! The export built-in (without the `-p` option) exports each of the specified +//! names to the environment, with optional values. If no names are given, or if +//! the `-p` option is given, the names and values of all exported variables are +//! displayed. +//! +//! # Options +//! +//! The **`-p`** (**`--print`**) option causes the shell to display the names and +//! values of all exported variables in a format that can be reused as input to +//! restore the state of these variables. When used with operands, the option +//! limits the output to the specified variables. +//! +//! (TODO: Other non-portable options) +//! +//! # Operands +//! +//! The operands are names of shell variables to be exported. Each name may +//! optionally be followed by `=` and a *value* to assign to the variable. +//! +//! # Exit status +//! +//! Zero unless an error occurs. +//! +//! # Errors +//! +//! When exporting a variable with a value, it is an error if the variable is +//! read-only. +//! +//! When printing variables, it is an error if an operand names a non-existing +//! variable. +//! +//! # Portability +//! +//! This built-in is part of the POSIX standard. Printing variables is portable +//! only when the `-p` option is used without operands. +//! +//! # Implementation notes +//! +//! The implementation of this built-in depends on that of the +//! [`typeset`](crate::typeset) built-in. + +use crate::common::output; +use crate::common::report_error; +use crate::common::report_failure; +use crate::typeset::syntax::interpret; +use crate::typeset::syntax::parse; +use crate::typeset::syntax::OptionSpec; +use crate::typeset::syntax::PRINT_OPTION; +use crate::typeset::to_message; +use crate::typeset::Command; +use crate::typeset::Scope::Global; +use crate::typeset::VariableAttr::Export; +use yash_env::option::State::On; +use yash_env::semantics::Field; +use yash_env::Env; + +/// List of portable options applicable to the export built-in +pub static PORTABLE_OPTIONS: &[OptionSpec<'static>] = &[PRINT_OPTION]; + +/// Entry point of the export built-in +pub async fn main(env: &mut Env, args: Vec) -> yash_env::builtin::Result { + match parse(PORTABLE_OPTIONS, args) { + Ok((options, operands)) => match interpret(options, operands) { + Ok(mut command) => { + match &mut command { + Command::SetVariables(sv) => { + sv.attrs.push((Export, On)); + sv.scope = Global; + } + // TODO print as export rather than typeset + Command::PrintVariables(pv) => { + pv.attrs.push((Export, On)); + pv.scope = Global; + } + Command::SetFunctions(sf) => unreachable!("{sf:?}"), + Command::PrintFunctions(pf) => unreachable!("{pf:?}"), + } + match command.execute(env) { + Ok(result) => output(env, &result).await, + Err(errors) => report_failure(env, to_message(&errors)).await, + } + } + Err(error) => report_error(env, &error).await, + }, + Err(error) => report_error(env, &error).await, + } +} diff --git a/yash-builtin/src/lib.rs b/yash-builtin/src/lib.rs index 85021ed0..ffa439f9 100644 --- a/yash-builtin/src/lib.rs +++ b/yash-builtin/src/lib.rs @@ -50,6 +50,7 @@ pub mod r#continue; #[cfg(feature = "yash-semantics")] pub mod exec; pub mod exit; +pub mod export; pub mod jobs; pub mod pwd; pub mod readonly; @@ -118,6 +119,13 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ execute: |env, args| Box::pin(exit::main(env, args)), }, ), + ( + "export", + Builtin { + r#type: Special, + execute: |env, args| Box::pin(export::main(env, args)), + }, + ), ( "jobs", Builtin { diff --git a/yash-builtin/src/typeset.rs b/yash-builtin/src/typeset.rs index 57fd26de..bde8c05e 100644 --- a/yash-builtin/src/typeset.rs +++ b/yash-builtin/src/typeset.rs @@ -571,8 +571,11 @@ impl std::fmt::Display for ExecuteError { /// /// The first error's title is used as the message title. The other errors are /// added as annotations. +/// +/// This is a utility for printing errors returned by [`Command::execute`]. +/// The returned message can be passed to [`report_failure`]. #[must_use] -fn to_message(errors: &[ExecuteError]) -> Message { +pub fn to_message(errors: &[ExecuteError]) -> Message { let mut message = Message::from(&errors[0]); let other_errors = errors[1..].iter().map(ExecuteError::main_annotation); message.annotations.extend(other_errors); From f77c07de629fa49838876823dc514dc23e327657 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Mon, 20 Nov 2023 01:26:39 +0900 Subject: [PATCH 2/6] export: Print variables with built-in's own name The export built-in should print variables with the name "export" instead of "typeset". This commit introduces a new struct PrintVariablesContext that contains the name to be used when printing variables. --- yash-builtin/src/export.rs | 9 +- yash-builtin/src/typeset.rs | 24 ++++- yash-builtin/src/typeset/print_variables.rs | 105 +++++++++++++++----- 3 files changed, 108 insertions(+), 30 deletions(-) diff --git a/yash-builtin/src/export.rs b/yash-builtin/src/export.rs index 07ffbe80..53e7ac84 100644 --- a/yash-builtin/src/export.rs +++ b/yash-builtin/src/export.rs @@ -76,6 +76,7 @@ use crate::typeset::syntax::OptionSpec; use crate::typeset::syntax::PRINT_OPTION; use crate::typeset::to_message; use crate::typeset::Command; +use crate::typeset::PrintVariablesContext; use crate::typeset::Scope::Global; use crate::typeset::VariableAttr::Export; use yash_env::option::State::On; @@ -85,6 +86,11 @@ use yash_env::Env; /// List of portable options applicable to the export built-in pub static PORTABLE_OPTIONS: &[OptionSpec<'static>] = &[PRINT_OPTION]; +/// Variable printing context for the export built-in +pub const PRINT_VARIABLES_CONTEXT: PrintVariablesContext<'static> = PrintVariablesContext { + builtin_name: "export", +}; + /// Entry point of the export built-in pub async fn main(env: &mut Env, args: Vec) -> yash_env::builtin::Result { match parse(PORTABLE_OPTIONS, args) { @@ -95,7 +101,6 @@ pub async fn main(env: &mut Env, args: Vec) -> yash_env::builtin::Result sv.attrs.push((Export, On)); sv.scope = Global; } - // TODO print as export rather than typeset Command::PrintVariables(pv) => { pv.attrs.push((Export, On)); pv.scope = Global; @@ -103,7 +108,7 @@ pub async fn main(env: &mut Env, args: Vec) -> yash_env::builtin::Result Command::SetFunctions(sf) => unreachable!("{sf:?}"), Command::PrintFunctions(pf) => unreachable!("{pf:?}"), } - match command.execute(env) { + match command.execute(env, &PRINT_VARIABLES_CONTEXT) { Ok(result) => output(env, &result).await, Err(errors) => report_failure(env, to_message(&errors)).await, } diff --git a/yash-builtin/src/typeset.rs b/yash-builtin/src/typeset.rs index bde8c05e..e2e7a7e0 100644 --- a/yash-builtin/src/typeset.rs +++ b/yash-builtin/src/typeset.rs @@ -339,6 +339,18 @@ pub struct PrintVariables { pub scope: Scope, } +/// Set of information used when printing variables +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PrintVariablesContext<'a> { + /// Name of the built-in printed as the command name + pub builtin_name: &'a str, +} + +/// Variable printing context for the typeset built-in +pub const PRINT_VARIABLES_CONTEXT: PrintVariablesContext<'static> = PrintVariablesContext { + builtin_name: "typeset", +}; + /// Attribute that can be set on a function #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] @@ -423,10 +435,16 @@ impl Command { /// If there are no errors, the method returns a string that should be /// printed to the standard output. /// Otherwise, the method returns a non-empty vector of errors. - pub fn execute(self, env: &mut Env) -> Result> { + pub fn execute( + self, + env: &mut Env, + print_variables_context: &PrintVariablesContext, + ) -> Result> { match self { Self::SetVariables(command) => command.execute(env), - Self::PrintVariables(command) => command.execute(&env.variables), + Self::PrintVariables(command) => { + command.execute(&env.variables, print_variables_context) + } Self::SetFunctions(command) => command.execute(&mut env.functions), Self::PrintFunctions(command) => command.execute(&env.functions), } @@ -586,7 +604,7 @@ pub fn to_message(errors: &[ExecuteError]) -> Message { pub async fn main(env: &mut Env, args: Vec) -> yash_env::builtin::Result { match syntax::parse(syntax::ALL_OPTIONS, args) { Ok((options, operands)) => match syntax::interpret(options, operands) { - Ok(command) => match command.execute(env) { + Ok(command) => match command.execute(env, &PRINT_VARIABLES_CONTEXT) { Ok(result) => output(env, &result).await, Err(errors) => report_failure(env, to_message(&errors)).await, }, diff --git a/yash-builtin/src/typeset/print_variables.rs b/yash-builtin/src/typeset/print_variables.rs index 674bc679..389954e7 100644 --- a/yash-builtin/src/typeset/print_variables.rs +++ b/yash-builtin/src/typeset/print_variables.rs @@ -20,7 +20,11 @@ use yash_env::variable::{Value, VariableSet}; impl PrintVariables { /// Executes the command. - pub fn execute(self, variables: &VariableSet) -> Result> { + pub fn execute( + self, + variables: &VariableSet, + context: &PrintVariablesContext, + ) -> Result> { let mut output = String::new(); let mut errors = Vec::new(); @@ -29,12 +33,12 @@ impl PrintVariables { // TODO Honor the collation order in the locale. variables.sort_unstable_by_key(|&(name, _)| name); for (name, var) in variables { - print_one(name, var, &self.attrs, &mut output); + print_one(name, var, &self.attrs, context, &mut output); } } else { for name in self.variables { match variables.get_scoped(&name.value, self.scope.into()) { - Some(var) => print_one(&name.value, var, &self.attrs, &mut output), + Some(var) => print_one(&name.value, var, &self.attrs, context, &mut output), None => errors.push(ExecuteError::PrintUnsetVariable(name)), } } @@ -53,6 +57,7 @@ fn print_one( name: &str, var: &Variable, filter_attrs: &[(VariableAttr, State)], + context: &PrintVariablesContext, output: &mut String, ) { // Skip if the variable does not match the filter. @@ -69,7 +74,8 @@ fn print_one( match &var.value { Some(value @ Value::Scalar(_)) => writeln!( output, - "typeset {}{}={}", + "{} {}{}={}", + context.builtin_name, options, quoted_name, value.quote() @@ -81,11 +87,21 @@ fn print_one( let options = options.to_string(); if !options.is_empty() { - writeln!(output, "typeset {}{}", options, quoted_name).unwrap(); + writeln!( + output, + "{} {}{}", + context.builtin_name, options, quoted_name + ) + .unwrap(); } } - None => writeln!(output, "typeset {}{}", options, quoted_name).unwrap(), + None => writeln!( + output, + "{} {}{}", + context.builtin_name, options, quoted_name + ) + .unwrap(), } } @@ -98,6 +114,7 @@ struct AttributeOption<'a> { impl std::fmt::Display for AttributeOption<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // TODO Filter printable options if self.var.is_read_only() { f.write_str("-r ")?; } @@ -126,7 +143,7 @@ mod tests { scope: Scope::Global, }; - let output = pv.execute(&vars).unwrap(); + let output = pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(); assert_eq!(output, "typeset foo=value\n") } @@ -149,7 +166,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset first=1\n\ typeset second=2\n\ typeset third=3\n", @@ -168,7 +185,8 @@ mod tests { scope: Scope::Global, }; - assert_eq!(pv.execute(&vars).unwrap(), "a=(1 '2 2' 3)\n"); + let result = pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(); + assert_eq!(result, "a=(1 '2 2' 3)\n"); } #[test] @@ -181,7 +199,8 @@ mod tests { scope: Scope::Global, }; - assert_eq!(pv.execute(&vars).unwrap(), "typeset x\n"); + let result = pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(); + assert_eq!(result, "typeset x\n"); } #[test] @@ -201,7 +220,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset 'valueless$'\n\ typeset 'scalar$'='=;'\n\ 'array$'=('~' \"'\" '*?')\n", @@ -227,7 +246,7 @@ mod tests { }; assert_eq!( - pv.execute(&inner).unwrap(), + pv.execute(&inner, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset global='global value'\n\ typeset local='local value'\n", ); @@ -251,7 +270,7 @@ mod tests { attrs: vec![], scope: Scope::Local, }; - let output = pv.execute(&inner).unwrap(); + let output = pv.execute(&inner, &PRINT_VARIABLES_CONTEXT).unwrap(); assert_eq!(output, "typeset local='local value'\n"); let pv = PrintVariables { @@ -260,7 +279,7 @@ mod tests { scope: Scope::Local, }; assert_eq!( - pv.execute(&inner).unwrap_err(), + pv.execute(&inner, &PRINT_VARIABLES_CONTEXT).unwrap_err(), [ExecuteError::PrintUnsetVariable(Field::dummy("global"))] ); } @@ -288,7 +307,7 @@ mod tests { }; assert_eq!( - pv.execute(&inner).unwrap(), + pv.execute(&inner, &PRINT_VARIABLES_CONTEXT).unwrap(), // sorted by name "typeset one=1\n\ typeset three=3\n\ @@ -319,7 +338,7 @@ mod tests { }; assert_eq!( - pv.execute(&inner).unwrap(), + pv.execute(&inner, &PRINT_VARIABLES_CONTEXT).unwrap(), // sorted by name "typeset three=3\n\ typeset two=2\n", @@ -343,7 +362,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset -x x\n\ typeset -r y\n\ typeset -r -x z\n", @@ -370,7 +389,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset -x x=X\n\ typeset -r y=Y\n\ typeset -r -x z=Z\n", @@ -397,7 +416,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "x=(X)\n\ typeset -x x\n\ y=(Y)\n\ @@ -430,7 +449,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset -r b\n\ typeset -r -x c\n", ); @@ -446,7 +465,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset -x a\n\ typeset d\n", ); @@ -462,7 +481,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset -x a\n\ typeset -r -x c\n", ); @@ -478,7 +497,7 @@ mod tests { }; assert_eq!( - pv.execute(&vars).unwrap(), + pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(), "typeset -r b\n\ typeset d\n", ); @@ -493,7 +512,8 @@ mod tests { scope: Scope::Global, }; - assert_eq!(pv.execute(&vars).unwrap(), "typeset -r b\n"); + let result = pv.execute(&vars, &PRINT_VARIABLES_CONTEXT).unwrap(); + assert_eq!(result, "typeset -r b\n"); } #[test] @@ -506,12 +526,47 @@ mod tests { scope: Scope::Global, }; + let error = pv + .execute(&VariableSet::new(), &PRINT_VARIABLES_CONTEXT) + .unwrap_err(); assert_eq!( - pv.execute(&VariableSet::new()).unwrap_err(), + error, [ ExecuteError::PrintUnsetVariable(foo), ExecuteError::PrintUnsetVariable(bar) ] ); } + + mod non_default_context { + use super::*; + + #[test] + fn builtin_name() { + let mut vars = VariableSet::new(); + vars.get_or_new("foo", Scope::Global.into()) + .assign("value", None) + .unwrap(); + let bar = &mut vars.get_or_new("bar", Scope::Global.into()); + bar.assign(Value::array(["1", "2"]), None).unwrap(); + bar.make_read_only(Location::dummy("bar location")); + vars.get_or_new("baz", Scope::Global.into()); + let pv = PrintVariables { + variables: Field::dummies(["foo", "bar", "baz"]), + attrs: vec![], + scope: Scope::Global, + }; + let context = PrintVariablesContext { + builtin_name: "export", + }; + + assert_eq!( + pv.execute(&vars, &context).unwrap(), + "export foo=value\n\ + bar=(1 2)\n\ + export -r bar\n\ + export baz\n" + ); + } + } } From 804fcc29f699902c88e3fd149306e9c43b28f6f3 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Mon, 20 Nov 2023 23:25:37 +0900 Subject: [PATCH 3/6] export: Don't print options in output When the export built-in prints variables in the form of commands that invoke the export built-in, it should not include any options. This commit adds a new field to `PrintVariablesContext` to specify the options that may be printed. --- yash-builtin/src/export.rs | 1 + yash-builtin/src/typeset.rs | 8 ++ yash-builtin/src/typeset/print_variables.rs | 82 +++++++++++++++++++-- yash-builtin/src/typeset/syntax.rs | 2 +- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/yash-builtin/src/export.rs b/yash-builtin/src/export.rs index 53e7ac84..f1347a32 100644 --- a/yash-builtin/src/export.rs +++ b/yash-builtin/src/export.rs @@ -89,6 +89,7 @@ pub static PORTABLE_OPTIONS: &[OptionSpec<'static>] = &[PRINT_OPTION]; /// Variable printing context for the export built-in pub const PRINT_VARIABLES_CONTEXT: PrintVariablesContext<'static> = PrintVariablesContext { builtin_name: "export", + options_allowed: &[], }; /// Entry point of the export built-in diff --git a/yash-builtin/src/typeset.rs b/yash-builtin/src/typeset.rs index e2e7a7e0..111769b9 100644 --- a/yash-builtin/src/typeset.rs +++ b/yash-builtin/src/typeset.rs @@ -254,6 +254,7 @@ //! //! TBD +use self::syntax::OptionSpec; use crate::common::{output, report_error, report_failure}; use thiserror::Error; use yash_env::function::Function; @@ -340,15 +341,22 @@ pub struct PrintVariables { } /// Set of information used when printing variables +/// +/// [`PrintVariables::execute`] prints a list of commands that invoke a built-in +/// to recreate the variables. This context is used to determine the name of the +/// built-in and the attributes possibly indicated as options to the built-in. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PrintVariablesContext<'a> { /// Name of the built-in printed as the command name pub builtin_name: &'a str, + /// Options that may be printed for the built-in + pub options_allowed: &'a [OptionSpec<'a>], } /// Variable printing context for the typeset built-in pub const PRINT_VARIABLES_CONTEXT: PrintVariablesContext<'static> = PrintVariablesContext { builtin_name: "typeset", + options_allowed: self::syntax::ALL_OPTIONS, }; /// Attribute that can be set on a function diff --git a/yash-builtin/src/typeset/print_variables.rs b/yash-builtin/src/typeset/print_variables.rs index 389954e7..c112c76a 100644 --- a/yash-builtin/src/typeset/print_variables.rs +++ b/yash-builtin/src/typeset/print_variables.rs @@ -69,7 +69,10 @@ fn print_one( } // Do the formatting. - let options = AttributeOption { var }; + let options = AttributeOption { + var, + options_allowed: context.options_allowed, + }; let quoted_name = yash_quote::quoted(name); match &var.value { Some(value @ Value::Scalar(_)) => writeln!( @@ -109,17 +112,22 @@ fn print_one( /// variable attributes. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct AttributeOption<'a> { + /// Variable to be printed var: &'a Variable, + /// Options that are printed if they are set + options_allowed: &'a [OptionSpec<'a>], } impl std::fmt::Display for AttributeOption<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // TODO Filter printable options - if self.var.is_read_only() { - f.write_str("-r ")?; - } - if self.var.is_exported { - f.write_str("-x ")?; + for option in self.options_allowed { + if let Some(attr) = option.attr { + if let Ok(attr) = VariableAttr::try_from(attr) { + if attr.test(self.var).into() { + write!(f, "-{} ", option.short)?; + } + } + } } Ok(()) } @@ -540,6 +548,7 @@ mod tests { mod non_default_context { use super::*; + use crate::typeset::syntax::{EXPORT_OPTION, READONLY_OPTION}; #[test] fn builtin_name() { @@ -558,6 +567,7 @@ mod tests { }; let context = PrintVariablesContext { builtin_name: "export", + ..PRINT_VARIABLES_CONTEXT }; assert_eq!( @@ -568,5 +578,63 @@ mod tests { export baz\n" ); } + + #[test] + fn attrs_to_print() { + let mut vars = VariableSet::new(); + let mut a = vars.get_or_new("a", Scope::Global.into()); + a.assign("A", None).unwrap(); + let mut b = vars.get_or_new("b", Scope::Global.into()); + b.assign("B", None).unwrap(); + b.export(true); + let mut c = vars.get_or_new("c", Scope::Global.into()); + c.assign("C", None).unwrap(); + c.make_read_only(Location::dummy("c location")); + let mut d = vars.get_or_new("d", Scope::Global.into()); + d.assign("D", None).unwrap(); + d.export(true); + d.make_read_only(Location::dummy("d location")); + let pv = PrintVariables { + variables: vec![], + attrs: vec![], + scope: Scope::Global, + }; + + let context = PrintVariablesContext { + options_allowed: &[READONLY_OPTION], + ..PRINT_VARIABLES_CONTEXT + }; + assert_eq!( + pv.clone().execute(&vars, &context).unwrap(), + "typeset a=A\n\ + typeset b=B\n\ + typeset -r c=C\n\ + typeset -r d=D\n", + ); + + let context = PrintVariablesContext { + options_allowed: &[EXPORT_OPTION], + ..PRINT_VARIABLES_CONTEXT + }; + assert_eq!( + pv.clone().execute(&vars, &context).unwrap(), + "typeset a=A\n\ + typeset -x b=B\n\ + typeset c=C\n\ + typeset -x d=D\n", + ); + + let context = PrintVariablesContext { + options_allowed: &[], + ..PRINT_VARIABLES_CONTEXT + }; + assert_eq!( + pv.execute(&vars, &context).unwrap(), + "typeset a=A\n\ + typeset b=B\n\ + typeset c=C\n\ + typeset d=D\n", + ); + } } } diff --git a/yash-builtin/src/typeset/syntax.rs b/yash-builtin/src/typeset/syntax.rs index f85dac94..0f9ec9e1 100644 --- a/yash-builtin/src/typeset/syntax.rs +++ b/yash-builtin/src/typeset/syntax.rs @@ -122,7 +122,7 @@ pub const UNEXPORT_OPTION: OptionSpec<'static> = OptionSpec { }; /// List of all option specifications applicable to the typeset built-in -pub static ALL_OPTIONS: &[OptionSpec<'static>] = &[ +pub const ALL_OPTIONS: &[OptionSpec<'static>] = &[ FUNCTIONS_OPTION, GLOBAL_OPTION, PRINT_OPTION, From 2dffd56814811904b5829b66bb43355b084aeaa1 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Mon, 20 Nov 2023 23:37:31 +0900 Subject: [PATCH 4/6] Scripted test for export built-in Test script copied from: https://github.com/magicant/yash/blob/bd1fd3f1420569ce09551cfc6a8acfb9092fd6dc/tests/export-p.tst --- yash/tests/scripted_test.rs | 5 +++ yash/tests/scripted_test/export-p.sh | 49 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 yash/tests/scripted_test/export-p.sh diff --git a/yash/tests/scripted_test.rs b/yash/tests/scripted_test.rs index 03d76965..7f5c6cb6 100644 --- a/yash/tests/scripted_test.rs +++ b/yash/tests/scripted_test.rs @@ -106,6 +106,11 @@ fn exit_builtin() { run("exit-p.sh") } +#[test] +fn export_builtin() { + run("export-p.sh") +} + #[test] fn fnmatch() { run("fnmatch-p.sh") diff --git a/yash/tests/scripted_test/export-p.sh b/yash/tests/scripted_test/export-p.sh new file mode 100644 index 00000000..44be3b0e --- /dev/null +++ b/yash/tests/scripted_test/export-p.sh @@ -0,0 +1,49 @@ +# export-p.sh: test of the export built-in for any POSIX-compliant shell + +posix="true" + +test_oE -e 0 'exporting one variable' -e +export a=bar +echo 1 $a +sh -c 'echo 2 $a' +__IN__ +1 bar +2 bar +__OUT__ + +test_oE -e 0 'exporting many variables' -e +a=X b=B c=X +export a=A b c=C +echo 1 $a $b $c +sh -c 'echo 2 $a $b $c' +__IN__ +1 A B C +2 A B C +__OUT__ + +: TODO Needs the eval built-in <<\__OUT__ +test_oE -e 0 'reusing printed exported variables' +export a=A +e="$(export -p)" +unset a +a=X +eval "$e" +sh -c 'echo $a' +__IN__ +A +__OUT__ + +test_oE 'exporting with assignments' +a=A export b=B +echo $a +sh -c 'echo $b' +__IN__ +A +B +__OUT__ + +test_O -d -e n 'read-only variable cannot be re-assigned' +readonly a=1 +export a=2 +echo not reached # special built-in error kills non-interactive shell +__IN__ From 84e1bfc1d186258c93cd8ff697047b3512f72f43 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Mon, 20 Nov 2023 23:40:27 +0900 Subject: [PATCH 5/6] Enable scripted test that depends on export built-in --- yash/tests/scripted_test/simple-p.sh | 1 - yash/tests/scripted_test/typeset-y.sh | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/yash/tests/scripted_test/simple-p.sh b/yash/tests/scripted_test/simple-p.sh index 7f5ea48d..88673ec2 100644 --- a/yash/tests/scripted_test/simple-p.sh +++ b/yash/tests/scripted_test/simple-p.sh @@ -154,7 +154,6 @@ __IN__ f12 created __OUT__ -: TODO Needs the export built-in <<\__OUT__ test_o 'assignment-like command argument' export foo=F sh -c 'echo $1 $foo' X foo=bar diff --git a/yash/tests/scripted_test/typeset-y.sh b/yash/tests/scripted_test/typeset-y.sh index 05d9fe2d..a726f34a 100644 --- a/yash/tests/scripted_test/typeset-y.sh +++ b/yash/tests/scripted_test/typeset-y.sh @@ -52,7 +52,7 @@ __IN__ a=unset b=unset __OUT__ -: TODO Needs the export and readonly built-ins <<\__OUT__ +: TODO Needs the readonly built-in <<\__OUT__ test_oE -e 0 'printing all variables (no option)' -e typeset >/dev/null typeset | grep -q '^typeset -x PATH=' @@ -146,7 +146,7 @@ __OUT__ ) -: TODO Needs the export and readonly built-ins <<\__OUT__ +: TODO Needs the readonly built-in <<\__OUT__ test_oE -e 0 'printing all variables (-p)' -e typeset -p >/dev/null typeset -p | grep -q '^typeset -x PATH=' From f5dca5daa63a27e9b4cd120359841fef7c8648a2 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Tue, 21 Nov 2023 01:17:26 +0900 Subject: [PATCH 6/6] Improve doc --- yash-builtin/src/typeset.rs | 8 +++++++- yash/tests/scripted_test/export-p.sh | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/yash-builtin/src/typeset.rs b/yash-builtin/src/typeset.rs index 111769b9..59fb240d 100644 --- a/yash-builtin/src/typeset.rs +++ b/yash-builtin/src/typeset.rs @@ -252,7 +252,13 @@ //! //! # Implementation notes //! -//! TBD +//! The implementation of this built-in is also used by the +//! [`export`](crate::export) built-in. Functions that are common to both +//! built-ins are parameterized to support the different behaviors of the +//! built-ins. By customizing the contents of [`Command`] and the +//! [`PrintVariablesContext`] passed to [`Command::execute`], you can even +//! implement a new built-in that behaves differently from both `typeset` and +//! `export`. use self::syntax::OptionSpec; use crate::common::{output, report_error, report_failure}; diff --git a/yash/tests/scripted_test/export-p.sh b/yash/tests/scripted_test/export-p.sh index 44be3b0e..6760c578 100644 --- a/yash/tests/scripted_test/export-p.sh +++ b/yash/tests/scripted_test/export-p.sh @@ -35,7 +35,10 @@ __OUT__ test_oE 'exporting with assignments' a=A export b=B +# POSIX requires $a to persist after the export built-in, +# but it is unspecified whether $a is exported. echo $a +# $a does not affect $b being exported. sh -c 'echo $b' __IN__ A @@ -45,5 +48,7 @@ __OUT__ test_O -d -e n 'read-only variable cannot be re-assigned' readonly a=1 export a=2 -echo not reached # special built-in error kills non-interactive shell +# The export built-in fails because of the readonly variable. +# Since it is a special built-in, the non-interactive shell exits. +echo not reached __IN__