From fa9c6bad26382628b75e0815ac6da38c7b2a8a1c Mon Sep 17 00:00:00 2001 From: Ralf Schandl Date: Fri, 22 Dec 2023 23:00:39 +0100 Subject: [PATCH 1/2] First Try --- Cargo.lock | 26 +- Cargo.toml | 2 + script-test/test-invalid-utf8.sh | 59 ++- src/cmd_line.rs | 297 ------------ src/main.rs | 519 +-------------------- src/opt_def.rs | 34 +- src/parseargs.rs | 743 +++++++++++++++++++++++++++++++ src/shell_code.rs | 137 +++--- 8 files changed, 927 insertions(+), 890 deletions(-) delete mode 100644 src/cmd_line.rs create mode 100644 src/parseargs.rs diff --git a/Cargo.lock b/Cargo.lock index 75563c7..70da4e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", - "clap_lex", + "clap_lex 0.5.0", "strsim", ] @@ -148,6 +148,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + [[package]] name = "colorchoice" version = "1.0.0" @@ -234,6 +240,12 @@ dependencies = [ "either", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.147" @@ -279,7 +291,9 @@ version = "0.2.0-SNAPSHOT" dependencies = [ "assert_cmd", "clap", + "clap_lex 0.6.0", "predicates", + "stfu8", ] [[package]] @@ -393,6 +407,16 @@ dependencies = [ "syn", ] +[[package]] +name = "stfu8" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1310970b29733b601839578f8ba24991a97057dbedc4ac0decea835474054ee7" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "strsim" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index c199d1a..46b7e63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ strip = true # Strip symbols from binary [dependencies] clap = { version = "4.3.0", features = ["derive"] } +clap_lex = "0.6.0" +stfu8 = "0.2.6" [dev-dependencies] assert_cmd = "2.0.12" diff --git a/script-test/test-invalid-utf8.sh b/script-test/test-invalid-utf8.sh index 32781cf..8fe5d4f 100755 --- a/script-test/test-invalid-utf8.sh +++ b/script-test/test-invalid-utf8.sh @@ -21,17 +21,58 @@ elif [ "$TEST_SHELL" = "yash" ]; then echo "Skipped with shell Yash, as it can't handle invalid UTF-8" else - # The following should create some valid code on day - test_pa_code 'exit 1' -o "n:name=name" -- "$(printf '\303\050')" - test_pa_code 'exit 1' -o "n:name=name" -- "$(printf '\240\241')" - test_pa_code 'exit 1' -o "n:name=name" -- "$(printf '\342\050\241')" - test_pa_code 'exit 1' -o "n:name=name" -- "$(printf '\342\202\050')" - test_pa_code 'exit 1' -o "n:name=name" -- "$(printf '\360\050\214\274')" - test_pa_code 'exit 1' -o "n:name=name" -- "$(printf '\360\220\050\274')" - test_pa_code 'exit 1' -o "n:name=name" -- "$(printf '\360\050\214\050')" + inv_1="$(printf '\303\050')" + inv_2="$(printf '\240\241')" + inv_3="$(printf '\342\050\241')" + inv_4="$(printf '\342\202\050')" + inv_5="$(printf '\360\050\214\274')" + inv_6="$(printf '\360\220\050\274')" + inv_7="$(printf '\360\050\214\050')" - # The following will always do an error exit + test_pa 'test "$1" = "$inv_1"' -o "n:name=name" -- "$inv_1" + test_pa 'test "$1" = "$inv_2"' -o "n:name=name" -- "$inv_2" + test_pa 'test "$1" = "$inv_3"' -o "n:name=name" -- "$inv_3" + test_pa 'test "$1" = "$inv_4"' -o "n:name=name" -- "$inv_4" + test_pa 'test "$1" = "$inv_5"' -o "n:name=name" -- "$inv_5" + test_pa 'test "$1" = "$inv_6"' -o "n:name=name" -- "$inv_6" + test_pa 'test "$1" = "$inv_7"' -o "n:name=name" -- "$inv_7" + + test_pa 'test "$name" = "$inv_1"' -o "n:name=name" -- -n "$inv_1" + test_pa 'test "$name" = "$inv_2"' -o "n:name=name" -- -n "$inv_2" + test_pa 'test "$name" = "$inv_3"' -o "n:name=name" -- -n "$inv_3" + test_pa 'test "$name" = "$inv_4"' -o "n:name=name" -- -n "$inv_4" + test_pa 'test "$name" = "$inv_5"' -o "n:name=name" -- -n "$inv_5" + test_pa 'test "$name" = "$inv_6"' -o "n:name=name" -- -n "$inv_6" + test_pa 'test "$name" = "$inv_7"' -o "n:name=name" -- -n "$inv_7" + + test_pa 'test "$name" = "$inv_1"' -o "n:name=name" -- -n"$inv_1" + test_pa 'test "$name" = "$inv_2"' -o "n:name=name" -- -n"$inv_2" + test_pa 'test "$name" = "$inv_3"' -o "n:name=name" -- -n"$inv_3" + test_pa 'test "$name" = "$inv_4"' -o "n:name=name" -- -n"$inv_4" + test_pa 'test "$name" = "$inv_5"' -o "n:name=name" -- -n"$inv_5" + test_pa 'test "$name" = "$inv_6"' -o "n:name=name" -- -n"$inv_6" + test_pa 'test "$name" = "$inv_7"' -o "n:name=name" -- -n"$inv_7" + + + test_pa 'test "$name" = "$inv_1"' -o "n:name=name" -- --name "$inv_1" + test_pa 'test "$name" = "$inv_2"' -o "n:name=name" -- --name "$inv_2" + test_pa 'test "$name" = "$inv_3"' -o "n:name=name" -- --name "$inv_3" + test_pa 'test "$name" = "$inv_4"' -o "n:name=name" -- --name "$inv_4" + test_pa 'test "$name" = "$inv_5"' -o "n:name=name" -- --name "$inv_5" + test_pa 'test "$name" = "$inv_6"' -o "n:name=name" -- --name "$inv_6" + test_pa 'test "$name" = "$inv_7"' -o "n:name=name" -- --name "$inv_7" + + test_pa 'test "$name" = "$inv_1"' -o "n:name=name" -- --name="$inv_1" + test_pa 'test "$name" = "$inv_2"' -o "n:name=name" -- --name="$inv_2" + test_pa 'test "$name" = "$inv_3"' -o "n:name=name" -- --name="$inv_3" + test_pa 'test "$name" = "$inv_4"' -o "n:name=name" -- --name="$inv_4" + test_pa 'test "$name" = "$inv_5"' -o "n:name=name" -- --name="$inv_5" + test_pa 'test "$name" = "$inv_6"' -o "n:name=name" -- --name="$inv_6" + test_pa 'test "$name" = "$inv_7"' -o "n:name=name" -- --name="$inv_7" + + + # The following will always result in an error exit # The parseargs arguments must always be valid UTF-8. test_pa_code 'exit 1' -n "X$(printf '\303\050')Y" -o "n:name=name" -- test_pa_code 'exit 1' -a "X$(printf '\303\050')Y" -o "n:name=name" -- diff --git a/src/cmd_line.rs b/src/cmd_line.rs deleted file mode 100644 index 3d99157..0000000 --- a/src/cmd_line.rs +++ /dev/null @@ -1,297 +0,0 @@ -// -// Part of parseargs - a command line options parser for shell scripts -// -// Copyright (c) 2023 Ralf Schandl -// This code is licensed under MIT license (see LICENSE.txt for details). -// - -use std::fmt; - -/// Element extracted from the command line. -#[derive(Debug, PartialEq)] -pub enum CmdLineElement { - /// A short option like '-l' without the leading dash - ShortOption(char), - /// A long option like '--long' without the leading dashes - LongOption(String), - /// A long option (without the leading dashes) with value (from --option=value) - LongOptionValue(String, String), - /// A program argument. - Argument(String), - /// The option/arguments separator "--". Used by caller if the arguments before a '--' should - /// be handled differently than after it or it is just ignored. - Separator, -} - -impl fmt::Display for CmdLineElement { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CmdLineElement::ShortOption(c) => write!(f, "-{}", c), - CmdLineElement::LongOption(c) => write!(f, "--{}", c), - CmdLineElement::LongOptionValue(o, v) => write!(f, "--{}={}", o, v), - CmdLineElement::Argument(v) => write!(f, "'{}'", v), - CmdLineElement::Separator => write!(f, "--"), - } - } -} - -pub struct CmdLineTokenizer { - /// Vector with command line arguments - cmd_line_args: Vec, - /// Index of the next argument to process - cmd_line_args_idx: usize, - /// Whether to stop option processing on the first non-option. - posix: bool, - /// Whether to only returns Arguments. Switched to true when '--' is found - /// or on the first non-option if posix == true - args_only: bool, - /// Left over characters from combined short options. With -abc, this will - /// hold ['b', 'c']. - left_over: Vec, -} - -impl CmdLineTokenizer { - pub fn new(args: Vec, posix: bool) -> CmdLineTokenizer { - CmdLineTokenizer { - cmd_line_args: args, - cmd_line_args_idx: 0, - posix, - args_only: false, - left_over: Vec::new(), - } - } - - // Internal: get next part (separated string) from the command line. - fn next_part(&mut self) -> Option { - if self.cmd_line_args_idx >= self.cmd_line_args.len() { - None - } else { - let r = Some(self.cmd_line_args[self.cmd_line_args_idx].to_string()); - self.cmd_line_args_idx += 1; - r - } - } - - /// Returns the next command line element or `None`. - pub fn next(&mut self) -> Option { - if !self.left_over.is_empty() { - // next character from combined short options (-xyz) - let chr = self.left_over.remove(0); - Some(CmdLineElement::ShortOption(chr)) - } else if self.args_only { - self.next_part().map(CmdLineElement::Argument) - } else { - match self.next_part() { - None => None, - Some(s) => { - if s.eq("--") { - if self.args_only { - Some(CmdLineElement::Argument(s)) - } else { - self.args_only = true; - Some(CmdLineElement::Separator) - } - } else if s.eq("-") { - if self.posix { - self.args_only = true; - } - Some(CmdLineElement::Argument(s)) - } else if let Some(opt_str) = s.strip_prefix("--") { - // parse long option - if let Some(eq_idx) = opt_str.find('=') { - let name = opt_str[..eq_idx].to_string(); - let value = opt_str[eq_idx + 1..].to_string(); - Some(CmdLineElement::LongOptionValue(name, value)) - } else { - Some(CmdLineElement::LongOption(opt_str.to_string())) - } - } else if let Some(opt_str) = s.strip_prefix('-') { - // skip leading '-' - let mut cs = opt_str.chars(); - let chr = cs.next().unwrap(); - cs.for_each(|f| self.left_over.push(f)); - Some(CmdLineElement::ShortOption(chr)) - } else { - if self.posix { - self.args_only = true; - } - Some(CmdLineElement::Argument(s)) - } - } - } - } - } - - /// Returns an argument for a previous option. - /// Also handles combined options like `-ooutfile`. - pub fn get_option_argument(&mut self) -> Option { - if !self.left_over.is_empty() { - let ret = Some(self.left_over.clone().into_iter().collect()); - self.left_over.clear(); - ret - } else { - self.next_part() - } - } -} - -#[cfg(test)] -mod cmd_line_element_tests { - use crate::cmd_line::CmdLineElement; - - #[test] - fn test_to_string() { - assert_eq!("-d", format!("{}", CmdLineElement::ShortOption('d'))); - assert_eq!( - "--debug", - format!("{}", CmdLineElement::LongOption("debug".to_string())) - ); - assert_eq!( - "--file=output", - format!( - "{}", - CmdLineElement::LongOptionValue("file".to_string(), "output".to_string()) - ) - ); - assert_eq!( - "'filename'", - format!("{}", CmdLineElement::Argument("filename".to_string())) - ); - assert_eq!("--", format!("{}", CmdLineElement::Separator)); - } -} -#[cfg(test)] -mod arg_parser_tests { - use crate::cmd_line::{CmdLineElement, CmdLineTokenizer}; - - #[test] - fn test_normal() { - let args = [ - "-d", - "-o", - "outfile", - "--name=parseargs", - "--version", - "1.0", - "one", - "two", - ] - .map(String::from) - .to_vec(); - - let mut pa = CmdLineTokenizer::new(args, false); - - assert_eq!(Some(CmdLineElement::ShortOption('d')), pa.next()); - assert_eq!(Some(CmdLineElement::ShortOption('o')), pa.next()); - assert_eq!(Some("outfile".to_string()), pa.get_option_argument()); - assert_eq!( - Some(CmdLineElement::LongOptionValue( - "name".to_string(), - "parseargs".to_string() - )), - pa.next() - ); - assert_eq!( - Some(CmdLineElement::LongOption("version".to_string())), - pa.next() - ); - assert_eq!(Some("1.0".to_string()), pa.get_option_argument()); - assert_eq!(Some(CmdLineElement::Argument("one".to_string())), pa.next()); - assert_eq!(Some(CmdLineElement::Argument("two".to_string())), pa.next()); - assert_eq!(None, pa.next()); - } - - #[test] - fn test_mixed() { - let args = ["one", "-d", "-o", "outfile", "--name=parseargs", "two"] - .map(String::from) - .to_vec(); - - let mut pa = CmdLineTokenizer::new(args, false); - - assert_eq!(Some(CmdLineElement::Argument("one".to_string())), pa.next()); - assert_eq!(Some(CmdLineElement::ShortOption('d')), pa.next()); - assert_eq!(Some(CmdLineElement::ShortOption('o')), pa.next()); - assert_eq!(Some("outfile".to_string()), pa.get_option_argument()); - assert_eq!( - Some(CmdLineElement::LongOptionValue( - "name".to_string(), - "parseargs".to_string() - )), - pa.next() - ); - assert_eq!(Some(CmdLineElement::Argument("two".to_string())), pa.next()); - assert_eq!(None, pa.next()); - } - - #[test] - fn test_combined() { - let args = ["-dooutfile", "one"].map(String::from).to_vec(); - - let mut pa = CmdLineTokenizer::new(args, false); - - assert_eq!(Some(CmdLineElement::ShortOption('d')), pa.next()); - assert_eq!(Some(CmdLineElement::ShortOption('o')), pa.next()); - assert_eq!(Some("outfile".to_string()), pa.get_option_argument()); - assert_eq!(Some(CmdLineElement::Argument("one".to_string())), pa.next()); - assert_eq!(None, pa.next()); - } - - #[test] - fn test_dash_dash() { - let args = ["-d", "--", "-o"].map(String::from).to_vec(); - - let mut pa = CmdLineTokenizer::new(args, false); - - assert_eq!(Some(CmdLineElement::ShortOption('d')), pa.next()); - assert_eq!(Some(CmdLineElement::Separator), pa.next()); - assert_eq!(Some(CmdLineElement::Argument("-o".to_string())), pa.next()); - assert_eq!(None, pa.next()); - } - - #[test] - fn test_posix() { - let args = ["-d", "one", "-o", "outfile", "--name=parseargs", "two"] - .map(String::from) - .to_vec(); - - let mut pa = CmdLineTokenizer::new(args, true); - - assert_eq!(Some(CmdLineElement::ShortOption('d')), pa.next()); - assert_eq!(Some(CmdLineElement::Argument("one".to_string())), pa.next()); - assert_eq!(Some(CmdLineElement::Argument("-o".to_string())), pa.next()); - assert_eq!( - Some(CmdLineElement::Argument("outfile".to_string())), - pa.next() - ); - assert_eq!( - Some(CmdLineElement::Argument("--name=parseargs".to_string())), - pa.next() - ); - assert_eq!(Some(CmdLineElement::Argument("two".to_string())), pa.next()); - assert_eq!(None, pa.next()); - } - - #[test] - fn test_posix_with_dash() { - let args = ["-d", "-", "-o", "outfile", "--name=parseargs", "two"] - .map(String::from) - .to_vec(); - - let mut pa = CmdLineTokenizer::new(args, true); - - assert_eq!(Some(CmdLineElement::ShortOption('d')), pa.next()); - assert_eq!(Some(CmdLineElement::Argument("-".to_string())), pa.next()); - assert_eq!(Some(CmdLineElement::Argument("-o".to_string())), pa.next()); - assert_eq!( - Some(CmdLineElement::Argument("outfile".to_string())), - pa.next() - ); - assert_eq!( - Some(CmdLineElement::Argument("--name=parseargs".to_string())), - pa.next() - ); - assert_eq!(Some(CmdLineElement::Argument("two".to_string())), pa.next()); - assert_eq!(None, pa.next()); - } -} diff --git a/src/main.rs b/src/main.rs index f47ac54..7263bac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,35 +5,19 @@ // This code is licensed under MIT license (see LICENSE.txt for details). // -mod cmd_line; +//mod cmd_line; mod opt_def; +mod parseargs; mod shell_code; -use crate::shell_code::VarValue; -use shell_code::CodeChunk; -use std::cell::Cell; -use std::collections::HashMap; -use std::ffi::OsString; use std::io::{stdout, IsTerminal}; -use std::panic::catch_unwind; use std::process::exit; +use std::{ffi::OsString, panic::catch_unwind}; -use crate::cmd_line::{CmdLineElement, CmdLineTokenizer}; -use crate::opt_def::{OptConfig, OptTarget, OptType}; use clap::{CommandFactory, Parser}; -const PARSEARGS: &str = env!("CARGO_PKG_NAME"); const GIT_HASH: &str = env!("GIT_HASH_STATUS"); -/// The default shell, if '-s' is not given. -/// Can be overwritten using the environment variable 'PARSEARGS_SHELL'. -const DEFAULT_SHELL: &str = "sh"; - -/// Environment variable to set the default shell. -/// If '-s' is not given, this environment variable is checked. -/// If set, its value will be used as default shell. If not, 'sh' is used. -const PARSEARGS_SHELL_VAR: &str = "PARSEARGS_SHELL"; - /// Command line arguments. #[derive(Parser, Debug)] #[clap( @@ -105,14 +89,6 @@ struct CmdLineArgs { script_args: Vec, } -/// Exit after printing an error message. -fn die_internal(msg: String) -> ! { - eprintln!("{}: {}", PARSEARGS, msg); - println!("exit 1"); - - exit(11); -} - /// Used by Clap to validate a given str as shell variable/function name and to create a String from it. fn parse_shell_name(arg: &str) -> Result { for (idx, chr) in arg.chars().enumerate() { @@ -126,493 +102,6 @@ fn parse_shell_name(arg: &str) -> Result { Ok(arg.to_string()) } -/// Produces the initial shell code. Like checking that required functions really exist and -/// typesetting the variables (if supported by shell). -fn shell_init_code( - opt_cfg_list: &Vec, - cmd_line_args: &CmdLineArgs, - init_vars: bool, -) -> Vec { - let mut init_code: Vec = vec![]; - - // First function checks ... - if let Some(func) = &cmd_line_args.arg_callback { - init_code.push(CodeChunk::CheckForFunction(func.clone())); - } - if let Some(func) = &cmd_line_args.error_callback { - init_code.push(CodeChunk::CheckForFunction(func.clone())); - } - - // Iterating opt_cfg_list multiple time, but I want a certain order of - // the generated code. - - // prevent multiple checks for same function (ModeSwitch) - let mut func_name_vec: Vec<&String> = vec![]; - for opt_cfg in opt_cfg_list { - if let OptTarget::Function(name) = &opt_cfg.get_target() { - if !func_name_vec.contains(&name) { - init_code.push(CodeChunk::CheckForFunction(name.clone())); - func_name_vec.push(name); - } - } - } - // ... then typset and counter variables - let mut handled_vars: Vec = vec![]; - - for opt_cfg in opt_cfg_list { - let name = opt_cfg.get_target_name(); - - if opt_cfg.is_target_variable() { - if init_vars && !handled_vars.contains(&name) { - match &opt_cfg.opt_type { - OptType::Flag(_) | OptType::Assignment(_) | OptType::ModeSwitch(_, _) => { - init_code.push(CodeChunk::AssignVar( - name.clone(), - VarValue::StringValue("".to_string()), - )) - } - OptType::Counter(_) => { - init_code.push(CodeChunk::AssignVar(name.clone(), VarValue::IntValue(0))); - } - } - handled_vars.push(name.clone()); - } else if let OptType::Counter(_) = &opt_cfg.opt_type { - init_code.push(CodeChunk::AssignVar(name.clone(), VarValue::IntValue(0))); - } - } - } - - if let Some(array) = &cmd_line_args.remainder { - init_code.push(CodeChunk::DeclareArrayVar(array.clone())); - init_code.push(CodeChunk::AssignEmptyArray(array.clone())); - } - - init_code -} - -/// Optional String to bool. -/// -/// The values "true" and "yes" result in `true`. -/// The values "false" and "no" result in `false`. -/// Check is case-insensitive. -/// -/// `None` results in given default value. -fn optional_str_to_bool(ostr: Option<&String>, default: bool) -> Result { - match ostr { - Some(v) => match v.to_lowercase().trim() { - "true" | "yes" => Ok(true), - "false" | "no" => Ok(false), - _ => Err(format!("Invalid boolean value: '{}'", v)), - }, - None => Ok(default), - } -} - -/// Optional String to optional u16. -/// -/// Returns Err on invalid value. -/// If input is None results in None -fn optional_string_to_optional_u16(value: Option<&String>) -> Result, String> { - match value { - Some(v) => { - let cnt = match v.parse::() { - Ok(v) => v, - Err(_) => Err(format!("Invalid unsigned integer (0-65535): '{}'", v))?, - }; - Ok(Some(cnt)) - } - None => Ok(None), - } -} - -/// Assign a value to an option target. -/// Return either aCodeChunk for a variable assignment or a function call. -fn assign_target(target: &OptTarget, value: VarValue) -> CodeChunk { - match target { - OptTarget::Variable(name) => CodeChunk::AssignVar(name.clone(), value), - OptTarget::Function(name) => CodeChunk::CallFunction(name.clone(), value), - } -} - -/// Parses the shell arguments based on the given option definition. -/// Returns a vector of CodeChunks. -fn parse_shell_options( - opt_cfg_list: &mut Vec, - cmd_line_args: &CmdLineArgs, -) -> Result, String> { - let mut shell_code: Vec = vec![]; - let mut arguments: Vec = vec![]; - - // Lookup table from target name to position in vector. - // Needed for duplication checks of Mode-Switches. - let mut shell_name_table: HashMap> = HashMap::new(); - - for (pos, e) in opt_cfg_list.iter().enumerate() { - let name = &e.get_target_name(); - - if shell_name_table.contains_key(name) { - shell_name_table.get_mut(name).unwrap().push(pos); - } else { - shell_name_table.insert(name.clone(), vec![pos]); - } - } - - let mut script_args = vec![]; - for oss in &cmd_line_args.script_args { - let result = OsString::into_string(oss.clone()); - if let Ok(utf8) = result { - script_args.push(utf8); - } else { - Err(format!( - "Invalid UTF-8 char(s) in {:?}", - result.unwrap_err() - ))? - } - } - - let mut cl_tok = CmdLineTokenizer::new(script_args, cmd_line_args.posix); - - let mut after_separator = false; - let mut prev_counter: Option<(&OptTarget, u16)> = None; - - while let Some(e) = cl_tok.next() { - if let CmdLineElement::Separator = e { - prev_counter = counter_assign(&mut shell_code, prev_counter); - after_separator = true; - continue; - } else if let CmdLineElement::Argument(value) = e { - prev_counter = counter_assign(&mut shell_code, prev_counter); - if let (true, Some(array)) = (after_separator, &cmd_line_args.remainder) { - shell_code.push(CodeChunk::AddToArray( - array.clone(), - VarValue::StringValue(value), - )); - } else if let Some(func) = &cmd_line_args.arg_callback { - shell_code.push(CodeChunk::CallFunction( - func.clone(), - VarValue::StringValue(value), - )); - } else { - arguments.push(value); - } - } else { - let opt_value = match &e { - CmdLineElement::LongOptionValue(_, v) => Some(v), - _ => None, - }; - - let opt_config = opt_cfg_list.iter().find(|cfg| cfg.match_option(&e)); - - if opt_config.is_none() { - return Err(format!("Unknown option: {}", e)); - } else if let Some(oc) = opt_config { - // Check duplicate options. Counter options and options that trigger a function call - // can be used multiple times. - if oc.assigned.get() && !oc.is_duplicate_allowed() { - return Err(format!("Duplicate option: {} ({})", e, oc.options_string())); - } - oc.assigned.set(true); - - if oc.singleton { - shell_code.clear(); - } - - match &oc.opt_type { - OptType::Flag(target) => { - prev_counter = counter_assign(&mut shell_code, prev_counter); - let bool_val = VarValue::BoolValue(optional_str_to_bool(opt_value, true)?); - shell_code.push(assign_target(target, bool_val)); - } - OptType::ModeSwitch(target, value) => { - prev_counter = counter_assign(&mut shell_code, prev_counter); - if opt_value.is_some() { - Err(format!("{}: No value supported.", oc.options_string()))?; - } - // Conflict detection is done at end of processing. - shell_code - .push(assign_target(target, VarValue::StringValue(value.clone()))); - } - OptType::Assignment(target) => { - prev_counter = counter_assign(&mut shell_code, prev_counter); - let opt_arg = match opt_value { - Some(v) => Some(v.clone()), - None => cl_tok.get_option_argument(), - }; - if let Some(opt_arg) = opt_arg { - shell_code.push(assign_target(target, VarValue::StringValue(opt_arg))); - } else { - return Err(format!("Missing argument for: {}", e)); - } - } - OptType::Counter(target) => { - if let Some((prev_target, _)) = prev_counter { - if prev_target != target { - counter_assign(&mut shell_code, prev_counter); - } - } - - let value = optional_string_to_optional_u16(opt_value)?; - oc.count_value - .set(value.unwrap_or(oc.count_value.get() + 1)); - - prev_counter = Some((target, oc.count_value.get())); - } - } - - if oc.singleton { - shell_code.push(CodeChunk::Exit(0)); - return Ok(shell_code); - } - } - } - } - counter_assign(&mut shell_code, prev_counter); - - // Check duplicates for ModeSwitches - // and handle required - for name in shell_name_table.keys() { - if shell_name_table.get(name).unwrap().len() > 1 { - let mut used_tab = vec![]; - let mut all_tab = vec![]; - let mut required = false; - for idx in shell_name_table.get(name).unwrap() { - if opt_cfg_list[*idx].assigned.get() { - used_tab.push(opt_cfg_list[*idx].options_string()); - } - all_tab.push(opt_cfg_list[*idx].options_string()); - if opt_cfg_list[*idx].required { - required = true; - } - } - if used_tab.len() > 1 { - return Err(format!( - "Options are mutual exclusive: {}", - used_tab.join(", ") - )); - } - if required && used_tab.is_empty() { - return Err(format!( - "One of the following options is required: {}", - all_tab.join(", ") - )); - } - } - - for oc in &mut *opt_cfg_list { - match oc.opt_type { - OptType::ModeSwitch(_, _) => (), - _ => { - if oc.required && !oc.assigned.get() { - return Err(format!( - "Required option not found: {}", - oc.options_string() - )); - } - } - } - } - } - - shell_code.push(CodeChunk::SetArgs(arguments)); - - Ok(shell_code) -} - -/// If counter is not None, creates the counter assignment. -/// Always returns None -fn counter_assign<'a>( - shell_code: &mut Vec, - counter: Option<(&'a OptTarget, u16)>, -) -> Option<(&'a OptTarget, u16)> { - if let Some((target, value)) = counter { - shell_code.push(assign_target(target, VarValue::IntValue(value as i32))); - None - } else { - counter - } -} - -/// Validate the option definitions. -/// -/// Check for: -/// -/// * duplicate options -/// * duplicate usage of variables/functions (only allowed for ModeSwitch) -/// * ModeSwitch with same value -/// -/// Does not allow function and variable with same name. For a shell script -/// this should work, but in our context it is most likely an error. -fn validate_option_definitions(opt_def_list: &Vec) { - let mut all_short_options = String::new(); - let mut all_long_options: Vec<&String> = vec![]; - let mut all_variables: Vec<(String, bool, bool)> = vec![]; - let mut mode_values_map: HashMap> = HashMap::new(); - - for oc in opt_def_list { - for chr in oc.opt_chars.chars() { - match all_short_options.find(chr) { - Some(_) => { - die_internal(format!("Duplicate definition of option '-{}'", chr)); - } - None => { - all_short_options.push(chr); - } - } - } - for lng in &oc.opt_strings { - if all_long_options.contains(&lng) { - die_internal(format!("Duplicate definition of option '--{}'", lng)); - } else { - all_long_options.push(lng); - } - } - - let name = oc.get_target_name(); - let is_function = oc.is_target_function(); - let is_mode_switch = matches!(oc.opt_type, OptType::ModeSwitch(_, _)); - - match all_variables.iter().find(|x| x.0 == name) { - Some(o) => { - if is_mode_switch { - if !o.2 { - die_internal(format!("Duplicate usage of variable/function '{}'", name)); - } else if is_function != o.1 { - die_internal(format!( - "Used as variable and function in mod-switch option: '{}'", - name - )); - } - } else { - die_internal(format!("Duplicate usage of variable/function '{}'", name)); - } - } - None => { - all_variables.push((name.clone(), is_function, is_mode_switch)); - } - } - if let OptType::ModeSwitch(_, value) = &oc.opt_type { - if mode_values_map.contains_key(&name) { - if let Some(v) = mode_values_map.get(&name) { - if v.contains(&value) { - die_internal(format!("Duplicate value '{}' for mode '{}'", value, name)); - } - } - } else { - mode_values_map.insert(name.clone(), vec![value]); - } - } - } -} - -/// The actual parseargs logic. -/// -/// The function does not return but exit. -fn parseargs(cmd_line_args: CmdLineArgs) -> ! { - let script_name = match cmd_line_args.name { - Some(ref n) => n, - None => PARSEARGS, - }; - - // parse the option definition string - let result = if cmd_line_args.options_list.is_some() { - opt_def::parse(&cmd_line_args.options_list.clone().unwrap().join(",")) - } else { - Ok(Vec::new()) - }; - - let mut opt_cfg_list = match result { - Ok(list) => list, - Err(error) => { - die_internal(format!("Error parsing option definition:\n{}", error)); - } - }; - - validate_option_definitions(&opt_cfg_list); - - // Add support for `--help` if requested. - // As this is added to the end of the list, a custom '--help' has precedence. - if cmd_line_args.help_opt { - opt_cfg_list.push(OptConfig { - opt_chars: "".to_string(), - opt_strings: vec!["help".to_string()], - opt_type: OptType::Flag(OptTarget::Function("show_help".to_string())), - required: false, - singleton: true, - assigned: Cell::new(false), - count_value: Cell::new(0), - }); - } - // Add support for `--version` if requested. - // As this is added to the end of the list, a custom '--version' has precedence. - if cmd_line_args.version_opt { - opt_cfg_list.push(OptConfig { - opt_chars: "".to_string(), - opt_strings: vec!["version".to_string()], - opt_type: OptType::Flag(OptTarget::Function("show_version".to_string())), - required: false, - singleton: true, - assigned: Cell::new(false), - count_value: Cell::new(0), - }); - } - - if cmd_line_args.debug { - for oc in &opt_cfg_list { - eprintln!("{:?}", oc); - } - } - - // Determine shell. Either from option, environment var or the default. - let shell = cmd_line_args - .shell - .clone() - .unwrap_or(std::env::var(PARSEARGS_SHELL_VAR).unwrap_or(DEFAULT_SHELL.to_string())); - - if cmd_line_args.debug { - eprintln!("Shell: {}", shell); - } - - // get the shell templates - let shell_tmpl = shell_code::get_shell_template(shell.as_str()); - if shell_tmpl.is_none() { - die_internal(format!("Unknown shell '{}'", shell)); - } - let shell_tmpl = shell_tmpl.unwrap(); - - if !shell_tmpl.supports_arrays && cmd_line_args.remainder.is_some() { - die_internal(format!( - "Shell {} does not support arrays, so option -r/--remainder is not supported", - shell - )); - } - - let mut code: Vec = vec![]; - - // generate initialization code. Check for functions, initialize variables - let mut init_code = shell_init_code(&opt_cfg_list, &cmd_line_args, cmd_line_args.init_vars); - - // let options_code = parse_shell_options(&opt_cfg_list, &cmd_line_args); - let rc = match parse_shell_options(&mut opt_cfg_list, &cmd_line_args) { - Ok(mut c) => { - code.append(&mut init_code); - code.append(&mut c); - 0 - } - Err(msg) => { - eprintln!("{}: {}", script_name, msg); - if let Some(func) = cmd_line_args.error_callback { - code.push(CodeChunk::CallFunction(func, VarValue::None)); - } - code.push(CodeChunk::Exit(1)); - 1 - } - }; - - println!("{}", shell_tmpl.format_vector(&code)); - - exit(rc); -} - fn main() { match CmdLineArgs::try_parse() { Ok(c) => { @@ -632,7 +121,7 @@ fn main() { } // Catch a panic and print `exit 1` to exit the calling script. - match catch_unwind(|| parseargs(c)) { + match catch_unwind(|| parseargs::parseargs(c)) { // Ok should never be reached, as parseargs exits Ok(_) => exit(97), Err(_) => { diff --git a/src/opt_def.rs b/src/opt_def.rs index a9b06ad..1b116fc 100644 --- a/src/opt_def.rs +++ b/src/opt_def.rs @@ -5,7 +5,7 @@ // This code is licensed under MIT license (see LICENSE.txt for details). // -use crate::cmd_line::CmdLineElement; +//use crate::cmd_line::CmdLineElement; use std::cell::Cell; /// Target for a option. Parseargs either assigns a variable or calls @@ -63,13 +63,20 @@ impl OptConfig { /// /// TODO: This is the only reason why we import CmdLineElement. Could also /// be implemented for char or string. - pub fn match_option(&self, el: &CmdLineElement) -> bool { - match el { - CmdLineElement::ShortOption(c) => self.opt_chars.find(*c).is_some(), - CmdLineElement::LongOption(s) => self.opt_strings.contains(s), - CmdLineElement::LongOptionValue(s, _) => self.opt_strings.contains(s), - _ => false, - } + // pub fn match_option(&self, el: &CmdLineElement) -> bool { + // match el { + // CmdLineElement::ShortOption(c) => self.opt_chars.find(*c).is_some(), + // CmdLineElement::LongOption(s) => self.opt_strings.contains(s), + // CmdLineElement::LongOptionValue(s, _) => self.opt_strings.contains(s), + // _ => false, + // } + // } + pub fn match_option_long(&self, name: &String) -> bool { + self.opt_strings.contains(name) + } + + pub fn match_option_short(&self, chr: &char) -> bool { + self.opt_chars.find(*chr).is_some() } /// Returns whether duplicate usage of this option is allowed. @@ -632,6 +639,7 @@ fn parse_opt_def_list(ps: &mut ParserSource) -> Result, ParsingEr #[cfg(test)] mod unit_tests { + use super::*; fn get_od_debug() -> OptConfig { @@ -688,13 +696,9 @@ mod unit_tests { #[test] fn test_opt_config_flag() { let oc = get_od_debug(); - assert!(oc.match_option(&CmdLineElement::ShortOption('d'))); - assert!(oc.match_option(&CmdLineElement::LongOption("debug".to_string()))); - assert!(oc.match_option(&CmdLineElement::LongOptionValue( - "debug".to_string(), - "true".to_string() - ))); - assert!(!oc.match_option(&CmdLineElement::Separator)); + assert!(oc.match_option_short(&'d')); + assert!(oc.match_option_long(&"debug".to_string())); + assert!(oc.match_option_long(&"debug".to_string())); assert!(!oc.is_duplicate_allowed()); assert!(!oc.is_target_function()); diff --git a/src/parseargs.rs b/src/parseargs.rs new file mode 100644 index 0000000..15f01e6 --- /dev/null +++ b/src/parseargs.rs @@ -0,0 +1,743 @@ +//use crate::cmd_line::{CmdLineElement, CmdLineTokenizer}; +use crate::opt_def; +use crate::opt_def::{OptConfig, OptTarget, OptType}; +use crate::shell_code; +use crate::shell_code::CodeChunk; +use crate::shell_code::VarValue; +use crate::CmdLineArgs; +use std::cell::Cell; +use std::collections::HashMap; +use std::ffi::{OsStr, OsString}; +use std::io::{self, Write}; +use std::process::exit; + +const PARSEARGS: &str = env!("CARGO_PKG_NAME"); + +/// The default shell, if '-s' is not given. +/// Can be overwritten using the environment variable 'PARSEARGS_SHELL'. +const DEFAULT_SHELL: &str = "sh"; + +/// Environment variable to set the default shell. +/// If '-s' is not given, this environment variable is checked. +/// If set, its value will be used as default shell. If not, 'sh' is used. +const PARSEARGS_SHELL_VAR: &str = "PARSEARGS_SHELL"; + +/// The actual parseargs logic. +/// +/// The function does not return but exit. + +pub fn parseargs(cmd_line_args: CmdLineArgs) -> ! { + let script_name = match cmd_line_args.name { + Some(ref n) => n, + None => PARSEARGS, + }; + + // parse the option definition string + let result = if cmd_line_args.options_list.is_some() { + opt_def::parse(&cmd_line_args.options_list.clone().unwrap().join(",")) + } else { + Ok(Vec::new()) + }; + + let mut opt_cfg_list = match result { + Ok(list) => list, + Err(error) => { + die_internal(format!("Error parsing option definition:\n{}", error)); + } + }; + + validate_option_definitions(&opt_cfg_list); + + // Add support for `--help` if requested. + // As this is added to the end of the list, a custom '--help' has precedence. + if cmd_line_args.help_opt { + opt_cfg_list.push(OptConfig { + opt_chars: "".to_string(), + opt_strings: vec!["help".to_string()], + opt_type: OptType::Flag(OptTarget::Function("show_help".to_string())), + required: false, + singleton: true, + assigned: Cell::new(false), + count_value: Cell::new(0), + }); + } + // Add support for `--version` if requested. + // As this is added to the end of the list, a custom '--version' has precedence. + if cmd_line_args.version_opt { + opt_cfg_list.push(OptConfig { + opt_chars: "".to_string(), + opt_strings: vec!["version".to_string()], + opt_type: OptType::Flag(OptTarget::Function("show_version".to_string())), + required: false, + singleton: true, + assigned: Cell::new(false), + count_value: Cell::new(0), + }); + } + + if cmd_line_args.debug { + for oc in &opt_cfg_list { + eprintln!("{:?}", oc); + } + } + + // Determine shell. Either from option, environment var or the default. + let shell = cmd_line_args + .shell + .clone() + .unwrap_or(std::env::var(PARSEARGS_SHELL_VAR).unwrap_or(DEFAULT_SHELL.to_string())); + + if cmd_line_args.debug { + eprintln!("Shell: {}", shell); + } + + // get the shell templates + let shell_tmpl = shell_code::get_shell_template(shell.as_str()); + if shell_tmpl.is_none() { + die_internal(format!("Unknown shell '{}'", shell)); + } + let shell_tmpl = shell_tmpl.unwrap(); + + if !shell_tmpl.supports_arrays && cmd_line_args.remainder.is_some() { + die_internal(format!( + "Shell {} does not support arrays, so option -r/--remainder is not supported", + shell + )); + } + + let mut code: Vec = vec![]; + + // generate initialization code. Check for functions, initialize variables + let mut init_code = shell_init_code(&opt_cfg_list, &cmd_line_args, cmd_line_args.init_vars); + + let rc = match parse_shell_options(&mut opt_cfg_list, &cmd_line_args) { + Ok(mut c) => { + code.append(&mut init_code); + code.append(&mut c); + 0 + } + Err(msg) => { + eprintln!("{}: {}", script_name, msg); + if let Some(func) = cmd_line_args.error_callback { + code.push(CodeChunk::CallFunction(func, VarValue::None)); + } + code.push(CodeChunk::Exit(1)); + 1 + } + }; + + // println!("{}", shell_tmpl.format_vector(&code)); + let src_code = shell_tmpl.format_vector(&code); + + let bytes = match stfu8::decode_u8(&src_code) { + Ok(s) => s, + Err(e) => panic!("encoding/decoding failed of: {} - {}", src_code, e), + }; + + let _ = io::stdout().write_all(&bytes); + let _ = io::stdout().write(b"\n"); + + exit(rc); +} + +/// Exit after printing an error message. +fn die_internal(msg: String) -> ! { + eprintln!("{}: {}", PARSEARGS, msg); + println!("exit 1"); + + exit(11); +} + +/// Produces the initial shell code. Like checking that required functions really exist and +/// typesetting the variables (if supported by shell). +fn shell_init_code( + opt_cfg_list: &Vec, + cmd_line_args: &CmdLineArgs, + init_vars: bool, +) -> Vec { + let mut init_code: Vec = vec![]; + + // First function checks ... + if let Some(func) = &cmd_line_args.arg_callback { + init_code.push(CodeChunk::CheckForFunction(func.clone())); + } + if let Some(func) = &cmd_line_args.error_callback { + init_code.push(CodeChunk::CheckForFunction(func.clone())); + } + + // Iterating opt_cfg_list multiple time, but I want a certain order of + // the generated code. + + // prevent multiple checks for same function (ModeSwitch) + let mut func_name_vec: Vec<&String> = vec![]; + for opt_cfg in opt_cfg_list { + if let OptTarget::Function(name) = &opt_cfg.get_target() { + if !func_name_vec.contains(&name) { + init_code.push(CodeChunk::CheckForFunction(name.clone())); + func_name_vec.push(name); + } + } + } + // ... then typset and counter variables + let mut handled_vars: Vec = vec![]; + + for opt_cfg in opt_cfg_list { + let name = opt_cfg.get_target_name(); + + if opt_cfg.is_target_variable() { + if init_vars && !handled_vars.contains(&name) { + match &opt_cfg.opt_type { + OptType::Flag(_) | OptType::Assignment(_) | OptType::ModeSwitch(_, _) => { + init_code.push(CodeChunk::AssignVar( + name.clone(), + VarValue::StringValue(OsString::from("")), + )) + } + OptType::Counter(_) => { + init_code.push(CodeChunk::AssignVar(name.clone(), VarValue::IntValue(0))); + } + } + handled_vars.push(name.clone()); + } else if let OptType::Counter(_) = &opt_cfg.opt_type { + init_code.push(CodeChunk::AssignVar(name.clone(), VarValue::IntValue(0))); + } + } + } + + if let Some(array) = &cmd_line_args.remainder { + init_code.push(CodeChunk::DeclareArrayVar(array.clone())); + init_code.push(CodeChunk::AssignEmptyArray(array.clone())); + } + + init_code +} + +/// Validate the option definitions. +/// +/// Check for: +/// +/// * duplicate options +/// * duplicate usage of variables/functions (only allowed for ModeSwitch) +/// * ModeSwitch with same value +/// +/// Does not allow function and variable with same name. For a shell script +/// this should work, but in our context it is most likely an error. +fn validate_option_definitions(opt_def_list: &Vec) { + let mut all_short_options = String::new(); + let mut all_long_options: Vec<&String> = vec![]; + let mut all_variables: Vec<(String, bool, bool)> = vec![]; + let mut mode_values_map: HashMap> = HashMap::new(); + + for oc in opt_def_list { + for chr in oc.opt_chars.chars() { + match all_short_options.find(chr) { + Some(_) => { + die_internal(format!("Duplicate definition of option '-{}'", chr)); + } + None => { + all_short_options.push(chr); + } + } + } + for lng in &oc.opt_strings { + if all_long_options.contains(&lng) { + die_internal(format!("Duplicate definition of option '--{}'", lng)); + } else { + all_long_options.push(lng); + } + } + + let name = oc.get_target_name(); + let is_function = oc.is_target_function(); + let is_mode_switch = matches!(oc.opt_type, OptType::ModeSwitch(_, _)); + + match all_variables.iter().find(|x| x.0 == name) { + Some(o) => { + if is_mode_switch { + if !o.2 { + die_internal(format!("Duplicate usage of variable/function '{}'", name)); + } else if is_function != o.1 { + die_internal(format!( + "Used as variable and function in mod-switch option: '{}'", + name + )); + } + } else { + die_internal(format!("Duplicate usage of variable/function '{}'", name)); + } + } + None => { + all_variables.push((name.clone(), is_function, is_mode_switch)); + } + } + if let OptType::ModeSwitch(_, value) = &oc.opt_type { + if mode_values_map.contains_key(&name) { + if let Some(v) = mode_values_map.get(&name) { + if v.contains(&value) { + die_internal(format!("Duplicate value '{}' for mode '{}'", value, name)); + } + } + } else { + mode_values_map.insert(name.clone(), vec![value]); + } + } + } +} + +/// Optional String to bool. +/// +/// The values "true" and "yes" result in `true`. +/// The values "false" and "no" result in `false`. +/// Check is case-insensitive. +/// +/// `None` results in given default value. +fn optional_str_to_bool(ostr: Option<&OsStr>, default: bool) -> Result { + match ostr { + Some(v) => { + let str_value = match v.to_os_string().into_string() { + Ok(s) => Ok(s), + Err(s) => Err(format!("Can't parse as boolean: {:?}", s)), + }?; + + match str_value.to_lowercase().trim() { + "true" | "yes" => Ok(true), + "false" | "no" => Ok(false), + _ => Err(format!("Invalid boolean value: '{}'", str_value)), + } + } + None => Ok(default), + } +} + +/// Optional String to optional u16. +/// +/// Returns Err on invalid value. +/// If input is None results in None +fn optional_string_to_optional_u16(value: Option<&OsStr>) -> Result, String> { + match value { + Some(v) => { + let str_value = match v.to_os_string().into_string() { + Ok(s) => Ok(s), + Err(s) => Err(format!("Can't parse as number: {:?}", s)), + }?; + let cnt = match str_value.parse::() { + Ok(v) => v, + Err(_) => Err(format!( + "Invalid unsigned integer (0-65535): '{}'", + str_value + ))?, + }; + Ok(Some(cnt)) + } + None => Ok(None), + } +} + +/// Assign a value to an option target. +/// Return either aCodeChunk for a variable assignment or a function call. +fn assign_target(target: &OptTarget, value: VarValue) -> CodeChunk { + match target { + OptTarget::Variable(name) => CodeChunk::AssignVar(name.clone(), value), + OptTarget::Function(name) => CodeChunk::CallFunction(name.clone(), value), + } +} + +/// Parses the shell arguments based on the given option definition. +/// Returns a vector of CodeChunks. +fn parse_shell_options( + opt_cfg_list: &mut Vec, + cmd_line_args: &CmdLineArgs, +) -> Result, String> { + let mut shell_code: Vec = vec![]; + let mut arguments: Vec = vec![]; + + // Lookup table from target name to position in vector. + // Needed for duplication checks of Mode-Switches. + let mut shell_name_table: HashMap> = HashMap::new(); + + for (pos, e) in opt_cfg_list.iter().enumerate() { + let name = &e.get_target_name(); + + if shell_name_table.contains_key(name) { + shell_name_table.get_mut(name).unwrap().push(pos); + } else { + shell_name_table.insert(name.clone(), vec![pos]); + } + } + + //let mut script_args = vec![]; + //for oss in &cmd_line_args.script_args { + // let result = OsString::into_string(oss.clone()); + // if let Ok(utf8) = result { + // script_args.push(utf8); + // } else { + // Err(format!( + // "Invalid UTF-8 char(s) in {:?}", + // result.unwrap_err() + // ))? + // } + //} + + let raw_args = clap_lex::RawArgs::new(&cmd_line_args.script_args); + let mut cursor = raw_args.cursor(); + + let mut after_separator = false; + let mut prev_counter: Option<(&OptTarget, u16)> = None; + + while let Some(arg) = raw_args.next(&mut cursor) { + if !after_separator && arg.is_escape() { + prev_counter = counter_assign(&mut shell_code, prev_counter); + after_separator = true; + continue; + } else if after_separator { + prev_counter = counter_assign(&mut shell_code, prev_counter); + // Argument + let argument = arg.to_value_os().to_owned(); + if let (true, Some(array)) = (after_separator, &cmd_line_args.remainder) { + shell_code.push(CodeChunk::AddToArray( + array.clone(), + VarValue::StringValue(argument), + )); + } else if let Some(func) = &cmd_line_args.arg_callback { + shell_code.push(CodeChunk::CallFunction( + func.clone(), + VarValue::StringValue(argument), + )); + } else { + arguments.push(argument); + } + } else if let Some((name, value)) = arg.to_long() { + let opt_name = match name { + Ok(s) => s, + Err(e) => return Err(format!("Can't parse {:?}", e)), + }; + + let opt_name_str = format!("--{}", opt_name); + + let opt_config = opt_cfg_list + .iter() + .find(|cfg| cfg.match_option_long(&opt_name.to_owned())); + if opt_config.is_none() { + return Err(format!("Unknown option: {}", opt_name_str)); + } else if let Some(oc) = opt_config { + // Check duplicate options. Counter options and options that trigger a function call + // can be used multiple times. + if oc.assigned.get() && !oc.is_duplicate_allowed() { + return Err(format!( + "Duplicate option: {} ({})", + opt_name_str, + oc.options_string() + )); + } + oc.assigned.set(true); + + if oc.singleton { + shell_code.clear(); + } + + match &oc.opt_type { + OptType::Flag(target) => { + prev_counter = counter_assign(&mut shell_code, prev_counter); + let bool_val = VarValue::BoolValue(optional_str_to_bool(value, true)?); + shell_code.push(assign_target(target, bool_val)); + } + OptType::ModeSwitch(target, mode_value) => { + prev_counter = counter_assign(&mut shell_code, prev_counter); + if value.is_some() { + Err(format!("{}: No value supported.", oc.options_string()))?; + } + // Conflict detection is done at end of processing. + shell_code.push(assign_target( + target, + VarValue::StringValue(OsString::from(mode_value)), + )); + } + OptType::Assignment(target) => { + prev_counter = counter_assign(&mut shell_code, prev_counter); + let opt_arg = match value { + Some(v) => Some(v), + None => raw_args.next_os(&mut cursor), + }; + if let Some(opt_arg) = opt_arg { + shell_code.push(assign_target( + target, + VarValue::StringValue(opt_arg.to_owned()), + )); + } else { + return Err(format!("Missing argument for: {}", opt_name_str)); + } + } + OptType::Counter(target) => { + if let Some((prev_target, _)) = prev_counter { + if prev_target != target { + counter_assign(&mut shell_code, prev_counter); + } + } + + let value = optional_string_to_optional_u16(value)?; + oc.count_value + .set(value.unwrap_or(oc.count_value.get() + 1)); + + prev_counter = Some((target, oc.count_value.get())); + } + } + } + } else if let Some(mut shorts) = arg.to_short() { + while let Some(short) = shorts.next_flag() { + let opt_char = match short { + Ok(s) => s, + Err(e) => return Err(format!("Can't parse {:?}", e)), + }; + + let opt_char_str: String = format!("-{}", opt_char); + + let opt_config = opt_cfg_list + .iter() + .find(|cfg| cfg.match_option_short(&opt_char)); + if opt_config.is_none() { + return Err(format!("Unknown option: {}", opt_char_str)); + } else if let Some(oc) = opt_config { + // Check duplicate options. Counter options and options that trigger a function call + // can be used multiple times. + if oc.assigned.get() && !oc.is_duplicate_allowed() { + return Err(format!( + "Duplicate option: {} ({})", + opt_char_str, + oc.options_string() + )); + } + oc.assigned.set(true); + + if oc.singleton { + shell_code.clear(); + } + + match &oc.opt_type { + OptType::Flag(target) => { + prev_counter = counter_assign(&mut shell_code, prev_counter); + shell_code.push(assign_target(target, VarValue::BoolValue(true))); + } + OptType::ModeSwitch(target, mode_value) => { + prev_counter = counter_assign(&mut shell_code, prev_counter); + // Conflict detection is done at end of processing. + shell_code.push(assign_target( + target, + VarValue::StringValue(OsString::from(mode_value)), + )); + } + OptType::Assignment(target) => { + prev_counter = counter_assign(&mut shell_code, prev_counter); + + let opt_arg = match shorts.next_value_os() { + Some(v) => Some(v), + None => raw_args.next_os(&mut cursor), + }; + + if let Some(opt_arg) = opt_arg { + shell_code.push(assign_target( + target, + VarValue::StringValue(opt_arg.to_owned()), + )); + } else { + return Err(format!("Missing argument for: {}", opt_char_str)); + } + } + OptType::Counter(target) => { + if let Some((prev_target, _)) = prev_counter { + if prev_target != target { + counter_assign(&mut shell_code, prev_counter); + } + } + oc.count_value.set(oc.count_value.get() + 1); + + prev_counter = Some((target, oc.count_value.get())); + } + } + } + } + } else { + if cmd_line_args.posix { + after_separator = true; + } + prev_counter = counter_assign(&mut shell_code, prev_counter); + // Argument + let argument = arg.to_value_os().to_owned(); + if let (true, Some(array)) = (after_separator, &cmd_line_args.remainder) { + shell_code.push(CodeChunk::AddToArray( + array.clone(), + VarValue::StringValue(argument), + )); + } else if let Some(func) = &cmd_line_args.arg_callback { + shell_code.push(CodeChunk::CallFunction( + func.clone(), + VarValue::StringValue(argument), + )); + } else { + arguments.push(argument); + } + } + } + + // let mut cl_tok = CmdLineTokenizer::new(cmd_line_args.script_args.clone(), cmd_line_args.posix); + + // let mut after_separator = false; + // let mut prev_counter: Option<(&OptTarget, u16)> = None; + + // while let Some(e) = cl_tok.next()? { + // if let CmdLineElement::Separator = e { + // prev_counter = counter_assign(&mut shell_code, prev_counter); + // after_separator = true; + // continue; + // } else if let CmdLineElement::Argument(value) = e { + // prev_counter = counter_assign(&mut shell_code, prev_counter); + // if let (true, Some(array)) = (after_separator, &cmd_line_args.remainder) { + // shell_code.push(CodeChunk::AddToArray( + // array.clone(), + // VarValue::StringValue(value), + // )); + // } else if let Some(func) = &cmd_line_args.arg_callback { + // shell_code.push(CodeChunk::CallFunction( + // func.clone(), + // VarValue::StringValue(value), + // )); + // } else { + // arguments.push(value); + // } + // } else { + // let opt_value = match &e { + // CmdLineElement::LongOptionValue(_, v) => Some(v), + // _ => None, + // }; + + // let opt_config = opt_cfg_list.iter().find(|cfg| cfg.match_option_long(&name)); + + // if opt_config.is_none() { + // return Err(format!("Unknown option: {}", e)); + // } else if let Some(oc) = opt_config { + // // Check duplicate options. Counter options and options that trigger a function call + // // can be used multiple times. + // if oc.assigned.get() && !oc.is_duplicate_allowed() { + // return Err(format!("Duplicate option: {} ({})", e, oc.options_string())); + // } + // oc.assigned.set(true); + + // if oc.singleton { + // shell_code.clear(); + // } + + // match &oc.opt_type { + // OptType::Flag(target) => { + // prev_counter = counter_assign(&mut shell_code, prev_counter); + // let bool_val = VarValue::BoolValue(optional_str_to_bool(opt_value, true)?); + // shell_code.push(assign_target(target, bool_val)); + // } + // OptType::ModeSwitch(target, value) => { + // prev_counter = counter_assign(&mut shell_code, prev_counter); + // if opt_value.is_some() { + // Err(format!("{}: No value supported.", oc.options_string()))?; + // } + // // Conflict detection is done at end of processing. + // shell_code.push(assign_target( + // target, + // VarValue::StringValue(OsString::from(value)), + // )); + // } + // OptType::Assignment(target) => { + // prev_counter = counter_assign(&mut shell_code, prev_counter); + // let opt_arg = match opt_value { + // Some(v) => Some(v.clone()), + // None => cl_tok.get_option_argument(), + // }; + // if let Some(opt_arg) = opt_arg { + // shell_code.push(assign_target(target, VarValue::StringValue(opt_arg))); + // } else { + // return Err(format!("Missing argument for: {}", e)); + // } + // } + // OptType::Counter(target) => { + // if let Some((prev_target, _)) = prev_counter { + // if prev_target != target { + // counter_assign(&mut shell_code, prev_counter); + // } + // } + + // let value = optional_string_to_optional_u16(opt_value)?; + // oc.count_value + // .set(value.unwrap_or(oc.count_value.get() + 1)); + + // prev_counter = Some((target, oc.count_value.get())); + // } + // } + + // if oc.singleton { + // shell_code.push(CodeChunk::Exit(0)); + // return Ok(shell_code); + // } + // } + // } + // } + counter_assign(&mut shell_code, prev_counter); + + // Check duplicates for ModeSwitches + // and handle required + for name in shell_name_table.keys() { + if shell_name_table.get(name).unwrap().len() > 1 { + let mut used_tab = vec![]; + let mut all_tab = vec![]; + let mut required = false; + for idx in shell_name_table.get(name).unwrap() { + if opt_cfg_list[*idx].assigned.get() { + used_tab.push(opt_cfg_list[*idx].options_string()); + } + all_tab.push(opt_cfg_list[*idx].options_string()); + if opt_cfg_list[*idx].required { + required = true; + } + } + if used_tab.len() > 1 { + return Err(format!( + "Options are mutual exclusive: {}", + used_tab.join(", ") + )); + } + if required && used_tab.is_empty() { + return Err(format!( + "One of the following options is required: {}", + all_tab.join(", ") + )); + } + } + + for oc in &mut *opt_cfg_list { + match oc.opt_type { + OptType::ModeSwitch(_, _) => (), + _ => { + if oc.required && !oc.assigned.get() { + return Err(format!( + "Required option not found: {}", + oc.options_string() + )); + } + } + } + } + } + + shell_code.push(CodeChunk::SetArgs(arguments)); + + Ok(shell_code) +} + +/// If counter is not None, creates the counter assignment. +/// Always returns None +fn counter_assign<'a>( + shell_code: &mut Vec, + counter: Option<(&'a OptTarget, u16)>, +) -> Option<(&'a OptTarget, u16)> { + if let Some((target, value)) = counter { + shell_code.push(assign_target(target, VarValue::IntValue(value as i32))); + None + } else { + counter + } +} diff --git a/src/shell_code.rs b/src/shell_code.rs index 099c28c..bd9675b 100644 --- a/src/shell_code.rs +++ b/src/shell_code.rs @@ -7,7 +7,12 @@ #![allow(unused)] -use std::{ascii::escape_default, fmt}; +use std::{ + ascii::escape_default, + ffi::{OsStr, OsString}, + fmt, + path::{Path, PathBuf}, +}; const SHELL_TRUE: &str = "'true'"; const SHELL_FALSE: &str = "''"; @@ -16,7 +21,7 @@ const SHELL_EXIT: &str = "exit"; /// VarValue represents a value that should be assigned to a shell variable /// or given as argument in a function call. pub enum VarValue { - StringValue(String), + StringValue(OsString), IntValue(i32), BoolValue(bool), None, @@ -25,37 +30,43 @@ impl VarValue { /// Escape a String for usage as shell value /// The value is enclosed in single quotes and a single quote in the value is replaced with /// "'\''". - fn escape_string(value: &str) -> String { + fn escape_string(value: &OsStr) -> String { + let encoded_str = stfu8::encode_u8(value.as_encoded_bytes()); + let mut esc = String::new(); esc.push('\''); - for c in value.chars() { + for c in encoded_str.chars() { if c == '\'' { - esc.push_str("'\\''"); + esc.push_str("'\\\\''"); } else { esc.push(c); } } esc.push('\''); + esc } } impl fmt::Display for VarValue { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let s: String = match self { - VarValue::StringValue(s) => VarValue::escape_string(s), - VarValue::IntValue(i) => i.to_string(), + match self { + VarValue::StringValue(s) => { + let esc = VarValue::escape_string(s); + let path = PathBuf::from(esc); + fmt.write_str(&format!("{}", path.display())) + } + VarValue::IntValue(i) => fmt.write_str(&i.to_string()), //VarValue::BoolValue(b) => if *b { "'true'".to_string() } else { "''".to_string() }, VarValue::BoolValue(b) => { if *b { - SHELL_TRUE.to_string() + fmt.write_str(SHELL_TRUE) } else { - SHELL_FALSE.to_string() + fmt.write_str(SHELL_FALSE) } } - VarValue::None => "".to_string(), - }; - fmt.write_str(&s) + VarValue::None => fmt.write_str(""), + } } } @@ -79,7 +90,7 @@ pub enum CodeChunk { CallFunction(String, VarValue), /// Set the positional parameter `$1`, `$2` ... - SetArgs(Vec), + SetArgs(Vec), /// Exit the calling script with the given exit code. Exit(i32), FalseReturn, @@ -191,13 +202,16 @@ impl CodeTemplates { } /// Format code with the replacement marker `{ARGS}` as a space-separated /// sequence of quoted strings. - fn format_code_args(&self, tmpl: &str, args: &[String]) -> String { + fn format_code_args(&self, tmpl: &str, args: &[OsString]) -> String { let mut args_str = String::new(); for (idx, a) in args.iter().enumerate() { if idx > 0 { args_str.push(' '); } - args_str.push_str(&VarValue::escape_string(a)); + let pathbuf = PathBuf::from(VarValue::escape_string(a)); + let value_fmt = format!("{}", pathbuf.display()); + + args_str.push_str(&value_fmt); } tmpl.replace("{ARGS}", &args_str).trim().to_string() } @@ -272,13 +286,15 @@ pub fn get_shell_template(shell: &str) -> Option<&CodeTemplates> { #[cfg(test)] mod var_value_tests { + use std::ffi::OsString; + use super::VarValue; #[test] fn test_string_escape_simple() { assert_eq!( "'test'".to_string(), - VarValue::StringValue("test".to_string()).to_string() + VarValue::StringValue(OsString::from("test")).to_string() ); } @@ -286,31 +302,31 @@ mod var_value_tests { fn test_string_escape_empty() { assert_eq!( "''".to_string(), - VarValue::StringValue("".to_string()).to_string() + VarValue::StringValue(OsString::from("")).to_string() ); } #[test] fn test_string_escape_quote() { assert_eq!( - "'don'\\''t'".to_string(), - VarValue::StringValue("don't".to_string()).to_string() + "'don'\\\\''t'".to_string(), + VarValue::StringValue(OsString::from("don't")).to_string() ); } #[test] fn test_string_escape_quote_border() { assert_eq!( - "''\\''do'\\'''".to_string(), - VarValue::StringValue("'do'".to_string()).to_string() + "''\\\\''do'\\\\'''".to_string(), + VarValue::StringValue(OsString::from("'do'")).to_string() ); } #[test] fn test_string_escape_quote_only() { assert_eq!( - "''\\'''\\'''\\'''".to_string(), - VarValue::StringValue("'''".to_string()).to_string() + "''\\\\'''\\\\'''\\\\'''".to_string(), + VarValue::StringValue(OsString::from("'''")).to_string() ); } @@ -318,7 +334,7 @@ mod var_value_tests { fn test_string_double_quote() { assert_eq!( "'\"hello\"'".to_string(), - VarValue::StringValue("\"hello\"".to_string()).to_string() + VarValue::StringValue(OsString::from("\"hello\"")).to_string() ); } @@ -381,8 +397,10 @@ mod shell_template_test { let var_name = "name".to_string(); - let chunk = - CodeChunk::AssignVar(var_name.clone(), VarValue::StringValue("value".to_string())); + let chunk = CodeChunk::AssignVar( + var_name.clone(), + VarValue::StringValue(OsString::from("value")), + ); assert_eq!("name='value'", shell.format(&chunk)); let chunk = CodeChunk::AssignVar(var_name.clone(), VarValue::IntValue(13)); @@ -396,19 +414,21 @@ mod shell_template_test { let var_name = "func".to_string(); - let chunk = - CodeChunk::CallFunction(var_name.clone(), VarValue::StringValue("value".to_string())); + let chunk = CodeChunk::CallFunction( + var_name.clone(), + VarValue::StringValue(OsString::from("value")), + ); assert_eq!("func 'value' || exit $?", shell.format(&chunk)); let chunk = CodeChunk::CheckForFunction(var_name.clone()); assert_eq!("if ! LC_ALL=C command -V func 2>/dev/null | head -n1 | grep function >/dev/null; then echo >&2 \"ERROR: Function 'func' does not exist.\"; exit 127; fi", shell.format(&chunk)); let chunk = CodeChunk::SetArgs(vec![ - "one".to_string(), - "don't".to_string(), - "count".to_string(), + OsString::from("one"), + OsString::from("don't"), + OsString::from("count"), ]); - assert_eq!("set -- 'one' 'don'\\''t' 'count'", shell.format(&chunk)); + assert_eq!("set -- 'one' 'don'\\\\''t' 'count'", shell.format(&chunk)); let chunk = CodeChunk::FalseReturn; assert_eq!("false", shell.format(&chunk)); @@ -425,7 +445,7 @@ mod shell_template_test { let chunk = CodeChunk::AssignEmptyArray(var_name.clone()); assert!(std::panic::catch_unwind(|| shell.format(&chunk)).is_err()); - let chunk = CodeChunk::AddToArray(var_name, VarValue::StringValue("test".to_string())); + let chunk = CodeChunk::AddToArray(var_name, VarValue::StringValue(OsString::from("test"))); assert!(std::panic::catch_unwind(|| shell.format(&chunk)).is_err()); } @@ -438,8 +458,10 @@ mod shell_template_test { let chunk = CodeChunk::DeclareArrayVar(var_name.clone()); assert_eq!("typeset -a name", shell.format(&chunk)); - let chunk = - CodeChunk::AssignVar(var_name.clone(), VarValue::StringValue("value".to_string())); + let chunk = CodeChunk::AssignVar( + var_name.clone(), + VarValue::StringValue(OsString::from("value")), + ); assert_eq!("name='value'", shell.format(&chunk)); let chunk = CodeChunk::AssignVar(var_name.clone(), VarValue::IntValue(13)); @@ -454,24 +476,26 @@ mod shell_template_test { let chunk = CodeChunk::AssignEmptyArray(var_name.clone()); assert_eq!("name=()", shell.format(&chunk)); - let chunk = CodeChunk::AddToArray(var_name, VarValue::StringValue("test".to_string())); + let chunk = CodeChunk::AddToArray(var_name, VarValue::StringValue(OsString::from("test"))); assert_eq!("name+=('test')", shell.format(&chunk)); let var_name = "func".to_string(); - let chunk = - CodeChunk::CallFunction(var_name.clone(), VarValue::StringValue("value".to_string())); + let chunk = CodeChunk::CallFunction( + var_name.clone(), + VarValue::StringValue(OsString::from("value")), + ); assert_eq!("func 'value' || exit $?", shell.format(&chunk)); let chunk = CodeChunk::CheckForFunction(var_name); assert_eq!("if ! typeset -f func >/dev/null 2>&1; then echo >&2 \"ERROR: Function 'func' does not exist.\"; exit 127; fi", shell.format(&chunk)); let chunk = CodeChunk::SetArgs(vec![ - "one".to_string(), - "don't".to_string(), - "count".to_string(), + OsString::from("one"), + OsString::from("don't"), + OsString::from("count"), ]); - assert_eq!("set -- 'one' 'don'\\''t' 'count'", shell.format(&chunk)); + assert_eq!("set -- 'one' 'don'\\\\''t' 'count'", shell.format(&chunk)); let chunk = CodeChunk::FalseReturn; assert_eq!("false", shell.format(&chunk)); @@ -489,8 +513,10 @@ mod shell_template_test { let chunk = CodeChunk::DeclareArrayVar(var_name.clone()); assert_eq!("typeset -a name", shell.format(&chunk)); - let chunk = - CodeChunk::AssignVar(var_name.clone(), VarValue::StringValue("value".to_string())); + let chunk = CodeChunk::AssignVar( + var_name.clone(), + VarValue::StringValue(OsString::from("value")), + ); assert_eq!("name='value'", shell.format(&chunk)); let chunk = CodeChunk::AssignVar(var_name.clone(), VarValue::IntValue(13)); @@ -505,24 +531,26 @@ mod shell_template_test { let chunk = CodeChunk::AssignEmptyArray(var_name.clone()); assert_eq!("set -A name", shell.format(&chunk)); - let chunk = CodeChunk::AddToArray(var_name, VarValue::StringValue("test".to_string())); + let chunk = CodeChunk::AddToArray(var_name, VarValue::StringValue(OsString::from("test"))); assert_eq!("name+=('test')", shell.format(&chunk)); let var_name = "func".to_string(); - let chunk = - CodeChunk::CallFunction(var_name.clone(), VarValue::StringValue("value".to_string())); + let chunk = CodeChunk::CallFunction( + var_name.clone(), + VarValue::StringValue(OsString::from("value")), + ); assert_eq!("func 'value' || exit $?", shell.format(&chunk)); let chunk = CodeChunk::CheckForFunction(var_name); assert_eq!("if ! typeset -f func >/dev/null 2>&1; then echo >&2 \"ERROR: Function 'func' does not exist.\"; exit 127; fi", shell.format(&chunk)); let chunk = CodeChunk::SetArgs(vec![ - "one".to_string(), - "don't".to_string(), - "count".to_string(), + OsString::from("one"), + OsString::from("don't"), + OsString::from("count"), ]); - assert_eq!("set -- 'one' 'don'\\''t' 'count'", shell.format(&chunk)); + assert_eq!("set -- 'one' 'don'\\\\''t' 'count'", shell.format(&chunk)); let chunk = CodeChunk::FalseReturn; assert_eq!("false", shell.format(&chunk)); @@ -537,7 +565,10 @@ mod shell_template_test { let var_name = "name".to_string(); - let c1 = CodeChunk::AssignVar(var_name.clone(), VarValue::StringValue("value".to_string())); + let c1 = CodeChunk::AssignVar( + var_name.clone(), + VarValue::StringValue(OsString::from("value")), + ); let c2 = CodeChunk::AssignVar(var_name.clone(), VarValue::IntValue(13)); let c3 = CodeChunk::AssignVar(var_name, VarValue::BoolValue(true)); From 9f4bdffbb3b6198b5ffde1db8cf57f3829563b18 Mon Sep 17 00:00:00 2001 From: Ralf Schandl Date: Sun, 24 Dec 2023 10:55:57 +0100 Subject: [PATCH 2/2] Script tests with ISO 8859-1 --- .github/workflows/release.yaml | 4 +- .github/workflows/snapshot-release.yaml | 4 +- .github/workflows/verify.yaml | 4 +- script-test/_test.shinc | 7 +++ script-test/run.sh | 6 ++- script-test/test-invalid-utf8.sh | 8 ++- script-test/test-iso8859-1-charset.sh | 68 +++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 13 deletions(-) create mode 100755 script-test/test-iso8859-1-charset.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 17b797d..0688098 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -83,12 +83,12 @@ jobs: - name: Install Shells (linux only) if: ${{ matrix.os=='linux' }} run: | - sudo apt-get install -y -qq ksh zsh yash + sudo apt-get install -y -qq ksh zsh yash x11-utils sudo apt-get install -y mksh || true - name: Install Shells & Shellcheck (macos only) if: ${{ matrix.os=='macos' }} - run: brew install bash ksh93 shellcheck + run: brew install bash ksh93 shellcheck luit - name: Install Shellcheck (windows only) if: ${{ matrix.os=='windows' }} diff --git a/.github/workflows/snapshot-release.yaml b/.github/workflows/snapshot-release.yaml index 542a38b..2497fda 100644 --- a/.github/workflows/snapshot-release.yaml +++ b/.github/workflows/snapshot-release.yaml @@ -77,12 +77,12 @@ jobs: - name: Install Shells (linux only) if: ${{ matrix.os=='linux' }} run: | - sudo apt-get install -y -qq ksh zsh yash + sudo apt-get install -y -qq ksh zsh yash x11-utils sudo apt-get install -y mksh || true - name: Install Shells & Shellcheck (macos only) if: ${{ matrix.os=='macos' }} - run: brew install bash ksh93 shellcheck + run: brew install bash ksh93 shellcheck luit - name: Install Shellcheck (windows only) if: ${{ matrix.os=='windows' }} diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 49aae89..a1b902b 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -38,12 +38,12 @@ jobs: - name: Install Shells (linux only) if: ${{ matrix.os=='linux' }} run: | - sudo apt-get install -y -qq ksh zsh yash + sudo apt-get install -y -qq ksh zsh yash x11-utils sudo apt-get install -y mksh || true - name: Install Shells & Shellcheck (macos only) if: ${{ matrix.os=='macos' }} - run: brew install bash ksh93 shellcheck + run: brew install bash ksh93 shellcheck luit - name: Install Shellcheck (windows only) if: ${{ matrix.os=='windows' }} diff --git a/script-test/_test.shinc b/script-test/_test.shinc index d973a98..cf3a5fb 100644 --- a/script-test/_test.shinc +++ b/script-test/_test.shinc @@ -52,6 +52,12 @@ start_test() echo "Testing: $(command -v parseargs)" } +skip_test() +{ + printf '\033[01;33mTEST SKIPPED - %s\033[0m\n' "$*" + exit 0 +} + end_test() { rc=0 @@ -99,6 +105,7 @@ test_pa() ok "parseargs $args" else failed "parseargs $args" + parseargs "$@" fi return $rc diff --git a/script-test/run.sh b/script-test/run.sh index 193536e..c0ece08 100755 --- a/script-test/run.sh +++ b/script-test/run.sh @@ -13,11 +13,13 @@ echo "Test Environment: $(uname -a)" case "$(uname -s | tr '[:upper:]' '[:lower:]')" in *cygwin*) IS_CYGWIN=TRUE - export IS_CYGWIN + IS_WINDOWS=TRUE + export IS_CYGWIN IS_WINDOWS ;; *msys* | *mingw*) IS_MSYS=TRUE - export IS_MSYS + IS_WINDOWS=TRUE + export IS_MSYS IS_WINDOWS ;; esac diff --git a/script-test/test-invalid-utf8.sh b/script-test/test-invalid-utf8.sh index 8fe5d4f..6ac4e12 100755 --- a/script-test/test-invalid-utf8.sh +++ b/script-test/test-invalid-utf8.sh @@ -15,13 +15,11 @@ script_name="$(basename "$0")" start_test -if [ -n "$IS_MSYS" ] || [ -n "$IS_CYGWIN" ]; then - echo "Skipped on Windows" +if [ -n "$IS_WINDOWS" ]; then + skip_test "on Windows" elif [ "$TEST_SHELL" = "yash" ]; then - echo "Skipped with shell Yash, as it can't handle invalid UTF-8" + skip_test "shell Yash, as it can't handle invalid UTF-8" else - - inv_1="$(printf '\303\050')" inv_2="$(printf '\240\241')" inv_3="$(printf '\342\050\241')" diff --git a/script-test/test-iso8859-1-charset.sh b/script-test/test-iso8859-1-charset.sh new file mode 100755 index 0000000..e1f9da0 --- /dev/null +++ b/script-test/test-iso8859-1-charset.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# +# Test parseargs with single byte +# +# shellcheck disable=SC2016 + +script_dir="$(cd "$(dirname "$0")" && pwd)" || exit 1 +script_name="$(basename "$0")" +script_file="$script_dir/$script_name" + +. "$script_dir/_test.shinc" + + +check_single_byte() +{ + # print the bytes for the UTF-8 Euro sign and check whether this results in + # a single character + x="$(printf '\342\202\254' | sed "s/./X/g")" + if [ ${#x} = 1 ]; then + return 1 + else + return 0 + fi +} + +if ! check_single_byte; then + # set ISO-8859-1 if available + SB_LOCALE=$(locale -a | grep 'iso88591$' | head -n1) + if [ -n "$SB_LOCALE" ]; then + echo "Setting LC_ALL to $SB_LOCALE" + export LC_ALL="$SB_LOCALE" + fi + + # If still no single byte char set -> exit + if ! check_single_byte; then + echo "No ISO-8859-1 character encoding available ... skipping" + exit 0 + fi + luit -encoding 'ISO 8859-1' "$script_file" + exit $? +fi + +start_test + +if [ -n "$IS_WINDOWS" ]; then + skip_test "on Windows" +elif [ "$TEST_SHELL" = "yash" ]; then + skip_test "shell Yash, can't handle this" +fi + +test_pa 'test "${#1}" = 3' -o "n:name=name" -- "$(printf '\342\202\254')" + +test_pa 'test "$1" = "�"' -o "n:name=name" -- � +test_pa 'test "$name" = "�"' -o "n:name=name" -- -n � +test_pa 'test "$name" = "�"' -o "n:name=name" -- -n� +test_pa 'test "$name" = "�"' -o "n:name=name" -- --name � +test_pa 'test "$name" = "�"' -o "n:name=name" -- --name=� + +test_pa 'test "$1" = "�'\''�"' -o "n:name=name" -- �\'� +test_pa 'test "$name" = "�'\''�"' -o "n:name=name" -- -n �\'� +test_pa 'test "$name" = "�'\''�"' -o "n:name=name" -- -n�\'� +test_pa 'test "$name" = "�'\''�"' -o "n:name=name" -- --name �\'� +test_pa 'test "$name" = "�'\''�"' -o "n:name=name" -- --name=�\'� + +end_test + +# vim:fileencoding=latin1 +