From 6051927c8fa32cfffa102d2a170c5a6cf747a1b9 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 9 Aug 2026 11:19:03 +0900 Subject: refactor(symfony-console): extract symfony/console into the shirabe-symfony-console crate Move `Symfony\Component\Console` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_console::application::Application` instead of `shirabe_external_packages::symfony::console::application::Application`. The `delegate_to_inner!` and `delegate_command_trait_impls_to_inner!` macros move with it. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/input/string_input.rs | 243 +++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 crates/shirabe-symfony-console/src/input/string_input.rs (limited to 'crates/shirabe-symfony-console/src/input/string_input.rs') diff --git a/crates/shirabe-symfony-console/src/input/string_input.rs b/crates/shirabe-symfony-console/src/input/string_input.rs new file mode 100644 index 00000000..720e2bef --- /dev/null +++ b/crates/shirabe-symfony-console/src/input/string_input.rs @@ -0,0 +1,243 @@ +//! ref: composer/vendor/symfony/console/Input/StringInput.php + +use crate::exception::invalid_argument_exception::InvalidArgumentException; +use crate::input::argv_input::ArgvInput; +use crate::input::input_definition::InputDefinition; +use crate::input::input_interface::InputInterface; +use crate::input::streamable_input_interface::StreamableInputInterface; +use indexmap::IndexMap; +use shirabe_php_shim::{CaptureKey, PhpMixed, php_regex}; + +/// StringInput represents an input provided as a string. +/// +/// Usage: +/// +/// ```php +/// $input = new StringInput('foo --bar="foobar"'); +/// ``` +#[derive(Debug, Clone)] +pub struct StringInput { + pub(crate) inner: ArgvInput, +} + +impl StringInput { + pub const REGEX_STRING: &'static str = r#"([^\s]+?)(?:\s|(? anyhow::Result { + // parent::__construct([]) + let inner = ArgvInput::new(Some(vec![]), None)?; + + let mut string_input = StringInput { inner }; + + let tokens = string_input.tokenize(input)?; + string_input.inner.set_tokens(tokens); + + Ok(string_input) + } + + /// Tokenizes a string. + fn tokenize(&self, input: &str) -> anyhow::Result> { + let bytes = input.as_bytes(); + let mut tokens: Vec = vec![]; + let length = shirabe_php_shim::strlen(input); + let mut cursor: i64 = 0; + let mut token: Option = None; + while cursor < length { + if bytes[cursor as usize] == b'\\' { + cursor += 1; + let next: String = match bytes.get(cursor as usize) { + Some(b) => String::from_utf8_lossy(&[*b]).into_owned(), + None => String::new(), + }; + token = Some(format!("{}{}", token.unwrap_or_default(), next)); + cursor += 1; + continue; + } + + let mut m: IndexMap> = IndexMap::new(); + if shirabe_php_shim::preg_match2( + php_regex!(r"/\s+/A"), + input, + &mut m, + 0, + cursor as usize, + ) { + if token.is_some() { + tokens.push(token.take().unwrap()); + } + cursor += + shirabe_php_shim::strlen(m[&CaptureKey::ByIndex(0)].as_deref().unwrap_or("")); + } else if shirabe_php_shim::preg_match2( + format!(r#"/([^="'\s]+?)(=?)({}+)/A"#, Self::REGEX_QUOTED_STRING), + input, + &mut m, + 0, + cursor as usize, + ) { + let inner = shirabe_php_shim::substr( + m[&CaptureKey::ByIndex(3)].as_deref().unwrap_or(""), + 1, + Some(-1), + ); + let replaced = + shirabe_php_shim::str_replace_arr(&["\"'", "'\"", "''", "\"\""], "", &inner); + token = Some(format!( + "{}{}{}{}", + token.unwrap_or_default(), + m[&CaptureKey::ByIndex(1)].as_deref().unwrap_or(""), + m[&CaptureKey::ByIndex(2)].as_deref().unwrap_or(""), + shirabe_php_shim::stripcslashes(&replaced) + )); + cursor += + shirabe_php_shim::strlen(m[&CaptureKey::ByIndex(0)].as_deref().unwrap_or("")); + } else if shirabe_php_shim::preg_match2( + format!(r"/{}/A", Self::REGEX_QUOTED_STRING), + input, + &mut m, + 0, + cursor as usize, + ) { + token = Some(format!( + "{}{}", + token.unwrap_or_default(), + shirabe_php_shim::stripcslashes(&shirabe_php_shim::substr( + m[&CaptureKey::ByIndex(0)].as_deref().unwrap_or(""), + 1, + Some(-1) + )) + )); + cursor += + shirabe_php_shim::strlen(m[&CaptureKey::ByIndex(0)].as_deref().unwrap_or("")); + } else if shirabe_php_shim::preg_match2( + format!(r"/{}/A", Self::REGEX_UNQUOTED_STRING), + input, + &mut m, + 0, + cursor as usize, + ) { + token = Some(format!( + "{}{}", + token.unwrap_or_default(), + m[&CaptureKey::ByIndex(1)].as_deref().unwrap_or("") + )); + cursor += + shirabe_php_shim::strlen(m[&CaptureKey::ByIndex(0)].as_deref().unwrap_or("")); + } else { + // should never happen + return Err(InvalidArgumentException::new(format!( + "Unable to parse input near \"... {} ...\".", + shirabe_php_shim::substr(input, cursor, Some(10)), + )) + .into()); + } + } + + if let Some(token) = token { + tokens.push(token); + } + + Ok(tokens) + } +} + +impl std::fmt::Display for StringInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.fmt(f) + } +} + +impl InputInterface for StringInput { + fn dup(&self) -> std::rc::Rc> { + std::rc::Rc::new(std::cell::RefCell::new(self.clone())) + } + + fn get_first_argument(&self) -> Option { + self.inner.get_first_argument() + } + + fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { + InputInterface::has_parameter_option(&self.inner, values, only_params) + } + + fn get_parameter_option( + &self, + values: PhpMixed, + default: PhpMixed, + only_params: bool, + ) -> PhpMixed { + InputInterface::get_parameter_option(&self.inner, values, default, only_params) + } + + fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> { + InputInterface::bind(&mut self.inner, definition) + } + + fn validate(&mut self) -> anyhow::Result<()> { + self.inner.validate() + } + + fn get_arguments(&self) -> IndexMap { + InputInterface::get_arguments(&self.inner) + } + + fn get_argument(&self, name: &str) -> anyhow::Result { + self.inner.get_argument(name) + } + + fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + self.inner.set_argument(name, value) + } + + fn has_argument(&self, name: &str) -> bool { + self.inner.has_argument(name) + } + + fn get_options(&self) -> IndexMap { + InputInterface::get_options(&self.inner) + } + + fn get_option(&self, name: &str) -> anyhow::Result { + self.inner.get_option(name) + } + + fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + self.inner.set_option(name, value) + } + + fn has_option(&self, name: &str) -> bool { + self.inner.has_option(name) + } + + fn is_interactive(&self) -> bool { + self.inner.is_interactive() + } + + fn set_interactive(&mut self, interactive: bool) { + self.inner.set_interactive(interactive) + } + + fn __to_string(&self) -> String { + self.to_string() + } + + fn as_streamable(&self) -> Option<&dyn StreamableInputInterface> { + Some(self) + } + + fn as_streamable_mut(&mut self) -> Option<&mut dyn StreamableInputInterface> { + Some(self) + } +} + +impl StreamableInputInterface for StringInput { + fn set_stream(&mut self, stream: shirabe_php_shim::PhpResource) { + self.inner.set_stream(stream) + } + + fn get_stream(&self) -> Option { + self.inner.get_stream() + } +} -- cgit v1.3.1-4-g156e