aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/input
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-12 01:01:35 +0900
committernsfisis <nsfisis@gmail.com>2026-06-12 01:01:43 +0900
commit1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch)
tree9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/input
parentddf0a624145b618c05c2ee47414545b99b685f37 (diff)
downloadphp-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/input')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/argv_input.rs643
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/array_input.rs379
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/input.rs252
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs91
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/input_aware_interface.rs5
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs470
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/input_interface.rs37
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/input_option.rs194
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/mod.rs6
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/streamable_input_interface.rs3
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/string_input.rs193
11 files changed, 2145 insertions, 128 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/argv_input.rs b/crates/shirabe-external-packages/src/symfony/console/input/argv_input.rs
new file mode 100644
index 0000000..e9cb8d3
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/input/argv_input.rs
@@ -0,0 +1,643 @@
+use crate::symfony::console::exception::runtime_exception::RuntimeException;
+use crate::symfony::console::input::input::Input;
+use crate::symfony::console::input::input_definition::InputDefinition;
+use crate::symfony::console::input::input_interface::InputInterface;
+use crate::symfony::console::input::streamable_input_interface::StreamableInputInterface;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+/// ArgvInput represents an input coming from the CLI arguments.
+///
+/// Usage:
+///
+/// $input = new ArgvInput();
+///
+/// By default, the `$_SERVER['argv']` array is used for the input values.
+///
+/// This can be overridden by explicitly passing the input values in the constructor:
+///
+/// $input = new ArgvInput($_SERVER['argv']);
+///
+/// If you pass it yourself, don't forget that the first element of the array
+/// is the name of the running application.
+///
+/// When passing an argument to the constructor, be sure that it respects
+/// the same rules as the argv one. It's almost always better to use the
+/// `StringInput` when you want to provide your own input.
+#[derive(Debug)]
+pub struct ArgvInput {
+ pub(crate) inner: Input,
+ tokens: Vec<String>,
+ parsed: Vec<String>,
+}
+
+impl ArgvInput {
+ pub fn new(
+ argv: Option<Vec<String>>,
+ definition: Option<InputDefinition>,
+ ) -> anyhow::Result<Self> {
+ // $argv = $argv ?? $_SERVER['argv'] ?? [];
+ let mut argv = match argv {
+ Some(argv) => argv,
+ None => std::env::args().collect(),
+ };
+
+ // strip the application name
+ if !argv.is_empty() {
+ argv.remove(0);
+ }
+
+ let mut input = ArgvInput {
+ inner: Input::new(None)?,
+ tokens: argv,
+ parsed: vec![],
+ };
+
+ // parent::__construct($definition)
+ match definition {
+ None => {}
+ Some(definition) => {
+ input.bind(&definition)?;
+ input.inner.validate()?;
+ }
+ }
+
+ Ok(input)
+ }
+
+ pub(crate) fn set_tokens(&mut self, tokens: Vec<String>) {
+ self.tokens = tokens;
+ }
+
+ pub fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> {
+ self.inner.arguments = IndexMap::new();
+ self.inner.options = IndexMap::new();
+ self.inner.definition = definition.clone();
+
+ self.parse()?;
+
+ Ok(())
+ }
+
+ fn parse(&mut self) -> anyhow::Result<()> {
+ let mut parse_options = true;
+ self.parsed = self.tokens.clone();
+ while !self.parsed.is_empty() {
+ let token = self.parsed.remove(0);
+ parse_options = self.parse_token(&token, parse_options)?;
+ }
+ Ok(())
+ }
+
+ pub(crate) fn parse_token(&mut self, token: &str, parse_options: bool) -> anyhow::Result<bool> {
+ if parse_options && token == "" {
+ self.parse_argument(token)?;
+ } else if parse_options && token == "--" {
+ return Ok(false);
+ } else if parse_options && shirabe_php_shim::str_starts_with(token, "--") {
+ self.parse_long_option(token)?;
+ } else if parse_options && token.as_bytes().first() == Some(&b'-') && token != "-" {
+ self.parse_short_option(token)?;
+ } else {
+ self.parse_argument(token)?;
+ }
+
+ Ok(parse_options)
+ }
+
+ /// Parses a short option.
+ fn parse_short_option(&mut self, token: &str) -> anyhow::Result<()> {
+ let name = shirabe_php_shim::substr(token, 1, None);
+
+ if shirabe_php_shim::strlen(&name) > 1 {
+ let first = shirabe_php_shim::substr(&name, 0, Some(1));
+ if self.inner.definition.has_shortcut(&first)
+ && self
+ .inner
+ .definition
+ .get_option_for_shortcut(&first)?
+ .accept_value()
+ {
+ // an option with a value (with no space)
+ self.add_short_option(
+ &first,
+ PhpMixed::String(shirabe_php_shim::substr(&name, 1, None)),
+ )?;
+ } else {
+ self.parse_short_option_set(&name)?;
+ }
+ } else {
+ self.add_short_option(&name, PhpMixed::Null)?;
+ }
+
+ Ok(())
+ }
+
+ /// Parses a short option set.
+ fn parse_short_option_set(&mut self, name: &str) -> anyhow::Result<()> {
+ let len = shirabe_php_shim::strlen(name);
+ let mut i = 0;
+ while i < len {
+ let name_i = shirabe_php_shim::substr(name, i, Some(1));
+ if !self.inner.definition.has_shortcut(&name_i) {
+ let encoding = shirabe_php_shim::mb_detect_encoding(name, None, true);
+ let bad = match encoding {
+ None => name_i.clone(),
+ Some(encoding) => {
+ shirabe_php_shim::mb_substr(name, i, Some(1), Some(&encoding))
+ }
+ };
+ return Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "The \"-%s\" option does not exist.",
+ &[PhpMixed::String(bad)],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ let option = self.inner.definition.get_option_for_shortcut(&name_i)?;
+ if option.accept_value() {
+ let value = if i == len - 1 {
+ PhpMixed::Null
+ } else {
+ PhpMixed::String(shirabe_php_shim::substr(name, i + 1, None))
+ };
+ self.add_long_option(&option.get_name().to_string(), value)?;
+
+ break;
+ } else {
+ self.add_long_option(&option.get_name().to_string(), PhpMixed::Null)?;
+ }
+ i += 1;
+ }
+
+ Ok(())
+ }
+
+ /// Parses a long option.
+ fn parse_long_option(&mut self, token: &str) -> anyhow::Result<()> {
+ let name = shirabe_php_shim::substr(token, 2, None);
+
+ match shirabe_php_shim::strpos(&name, "=") {
+ Some(pos) => {
+ let pos = pos as i64;
+ let value = shirabe_php_shim::substr(&name, pos + 1, None);
+ if value == "" {
+ self.parsed.insert(0, value.clone());
+ }
+ self.add_long_option(
+ &shirabe_php_shim::substr(&name, 0, Some(pos)),
+ PhpMixed::String(value),
+ )?;
+ }
+ None => {
+ self.add_long_option(&name, PhpMixed::Null)?;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Parses an argument.
+ fn parse_argument(&mut self, token: &str) -> anyhow::Result<()> {
+ let c = self.inner.arguments.len() as i64;
+
+ // if input is expecting another argument, add it
+ if self.inner.definition.has_argument(&PhpMixed::Int(c)) {
+ let arg = self.inner.definition.get_argument(&PhpMixed::Int(c))?;
+ let value = if arg.is_array() {
+ PhpMixed::List(vec![Box::new(PhpMixed::String(token.to_string()))])
+ } else {
+ PhpMixed::String(token.to_string())
+ };
+ self.inner
+ .arguments
+ .insert(arg.get_name().to_string(), value);
+
+ // if last argument isArray(), append token to last argument
+ } else if self.inner.definition.has_argument(&PhpMixed::Int(c - 1))
+ && self
+ .inner
+ .definition
+ .get_argument(&PhpMixed::Int(c - 1))?
+ .is_array()
+ {
+ let arg = self.inner.definition.get_argument(&PhpMixed::Int(c - 1))?;
+ if let Some(PhpMixed::List(list)) = self.inner.arguments.get_mut(arg.get_name()) {
+ list.push(Box::new(PhpMixed::String(token.to_string())));
+ }
+
+ // unexpected argument
+ } else {
+ let mut all = self.inner.definition.get_arguments().clone();
+ let mut symfony_command_name: Option<PhpMixed> = None;
+ let first_key = all.keys().next().cloned();
+ if let Some(key) = &first_key {
+ let input_argument = &all[key];
+ if input_argument.get_name() == "command" {
+ symfony_command_name = match self.inner.arguments.get("command") {
+ Some(v) => Some(v.clone()),
+ None => None,
+ };
+ all.shift_remove(key);
+ }
+ }
+
+ let message = if all.len() > 0 {
+ let names: Vec<String> = all.keys().cloned().collect();
+ match &symfony_command_name {
+ Some(symfony_command_name)
+ if !matches!(symfony_command_name, PhpMixed::Null) =>
+ {
+ shirabe_php_shim::sprintf(
+ "Too many arguments to \"%s\" command, expected arguments \"%s\".",
+ &[
+ symfony_command_name.clone(),
+ PhpMixed::String(shirabe_php_shim::implode("\" \"", &names)),
+ ],
+ )
+ }
+ _ => shirabe_php_shim::sprintf(
+ "Too many arguments, expected arguments \"%s\".",
+ &[PhpMixed::String(shirabe_php_shim::implode("\" \"", &names))],
+ ),
+ }
+ } else if symfony_command_name
+ .as_ref()
+ .map(|n| !matches!(n, PhpMixed::Null))
+ .unwrap_or(false)
+ {
+ shirabe_php_shim::sprintf(
+ "No arguments expected for \"%s\" command, got \"%s\".",
+ &[
+ symfony_command_name.clone().unwrap(),
+ PhpMixed::String(token.to_string()),
+ ],
+ )
+ } else {
+ shirabe_php_shim::sprintf(
+ "No arguments expected, got \"%s\".",
+ &[PhpMixed::String(token.to_string())],
+ )
+ };
+
+ return Err(
+ RuntimeException(shirabe_php_shim::RuntimeException { message, code: 0 }).into(),
+ );
+ }
+
+ Ok(())
+ }
+
+ /// Adds a short option value.
+ fn add_short_option(&mut self, shortcut: &str, value: PhpMixed) -> anyhow::Result<()> {
+ if !self.inner.definition.has_shortcut(shortcut) {
+ return Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "The \"-%s\" option does not exist.",
+ &[PhpMixed::String(shortcut.to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ self.add_long_option(
+ &self
+ .inner
+ .definition
+ .get_option_for_shortcut(shortcut)?
+ .get_name()
+ .to_string(),
+ value,
+ )
+ }
+
+ /// Adds a long option value.
+ fn add_long_option(&mut self, name: &str, mut value: PhpMixed) -> anyhow::Result<()> {
+ if !self.inner.definition.has_option(name) {
+ if !self.inner.definition.has_negation(name) {
+ return Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option does not exist.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ let option_name = self.inner.definition.negation_to_name(name)?;
+ if !matches!(value, PhpMixed::Null) {
+ return Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option does not accept a value.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+ self.inner
+ .options
+ .insert(option_name, PhpMixed::Bool(false));
+
+ return Ok(());
+ }
+
+ let option = self.inner.definition.get_option(name)?;
+
+ if !matches!(value, PhpMixed::Null) && !option.accept_value() {
+ return Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option does not accept a value.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ // in_array($value, ['', null], true)
+ let value_is_empty_or_null = matches!(&value, PhpMixed::String(s) if s.is_empty())
+ || matches!(value, PhpMixed::Null);
+ if value_is_empty_or_null && option.accept_value() && !self.parsed.is_empty() {
+ // if option accepts an optional or mandatory argument
+ // let's see if there is one provided
+ let next = self.parsed.remove(0);
+ // (isset($next[0]) && '-' !== $next[0]) || in_array($next, ['', null], true)
+ let next_first = next.as_bytes().first().copied();
+ if (next_first.is_some() && next_first != Some(b'-')) || next.is_empty() {
+ value = PhpMixed::String(next);
+ } else {
+ self.parsed.insert(0, next);
+ }
+ }
+
+ if matches!(value, PhpMixed::Null) {
+ if option.is_value_required() {
+ return Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option requires a value.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ if !option.is_array() && !option.is_value_optional() {
+ value = PhpMixed::Bool(true);
+ }
+ }
+
+ if option.is_array() {
+ match self.inner.options.get_mut(name) {
+ Some(PhpMixed::List(list)) => {
+ list.push(Box::new(value));
+ }
+ _ => {
+ self.inner
+ .options
+ .insert(name.to_string(), PhpMixed::List(vec![Box::new(value)]));
+ }
+ }
+ } else {
+ self.inner.options.insert(name.to_string(), value);
+ }
+
+ Ok(())
+ }
+
+ pub fn get_first_argument(&self) -> Option<String> {
+ let mut is_option = false;
+ for (i, token) in self.tokens.iter().enumerate() {
+ if !token.is_empty() && token.as_bytes()[0] == b'-' {
+ if shirabe_php_shim::str_contains(token, "=") || self.tokens.get(i + 1).is_none() {
+ continue;
+ }
+
+ // If it's a long option, consider that everything after "--" is the option name.
+ // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator)
+ let mut name = if token.as_bytes().get(1) == Some(&b'-') {
+ shirabe_php_shim::substr(token, 2, None)
+ } else {
+ shirabe_php_shim::substr(token, -1, None)
+ };
+ if !self.inner.options.contains_key(&name)
+ && !self.inner.definition.has_shortcut(&name)
+ {
+ // noop
+ } else {
+ if !self.inner.options.contains_key(&name) {
+ if let Ok(resolved) = self.inner.definition.shortcut_to_name(&name) {
+ name = resolved;
+ }
+ }
+ if let Some(option_value) = self.inner.options.get(&name) {
+ if self.tokens.get(i + 1).map(|t| t.as_str()) == option_value.as_string() {
+ is_option = true;
+ }
+ }
+ }
+
+ continue;
+ }
+
+ if is_option {
+ is_option = false;
+ continue;
+ }
+
+ return Some(token.clone());
+ }
+
+ None
+ }
+
+ pub fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool {
+ let values = to_array(values);
+
+ for token in &self.tokens {
+ if only_params && token == "--" {
+ return false;
+ }
+ for value in &values {
+ // Options with values:
+ // For long options, test for '--option=' at beginning
+ // For short options, test for '-o' at beginning
+ let leading = if shirabe_php_shim::str_starts_with(value, "--") {
+ format!("{}=", value)
+ } else {
+ value.clone()
+ };
+ if token == value
+ || (!leading.is_empty() && shirabe_php_shim::str_starts_with(token, &leading))
+ {
+ return true;
+ }
+ }
+ }
+
+ false
+ }
+
+ pub fn get_parameter_option(
+ &self,
+ values: PhpMixed,
+ default: PhpMixed,
+ only_params: bool,
+ ) -> PhpMixed {
+ let values = to_array(values);
+ let mut tokens = self.tokens.clone();
+
+ while !tokens.is_empty() {
+ let token = tokens.remove(0);
+ if only_params && token == "--" {
+ return default;
+ }
+
+ for value in &values {
+ if &token == value {
+ return match tokens.first() {
+ Some(_) => PhpMixed::String(tokens.remove(0)),
+ None => PhpMixed::Null,
+ };
+ }
+ // Options with values:
+ // For long options, test for '--option=' at beginning
+ // For short options, test for '-o' at beginning
+ let leading = if shirabe_php_shim::str_starts_with(value, "--") {
+ format!("{}=", value)
+ } else {
+ value.clone()
+ };
+ if !leading.is_empty() && shirabe_php_shim::str_starts_with(&token, &leading) {
+ return PhpMixed::String(shirabe_php_shim::substr(
+ &token,
+ shirabe_php_shim::strlen(&leading),
+ None,
+ ));
+ }
+ }
+ }
+
+ default
+ }
+
+ /// Returns a stringified representation of the args passed to the command.
+ pub fn to_string(&self) -> String {
+ let tokens: Vec<String> = self
+ .tokens
+ .iter()
+ .map(|token| {
+ if let Some(m) = shirabe_php_shim::preg_match_groups("{^(-[^=]+=)(.+)}", token) {
+ return format!("{}{}", m[1], self.inner.escape_token(&m[2]));
+ }
+
+ if !token.is_empty() && token.as_bytes()[0] != b'-' {
+ return self.inner.escape_token(token);
+ }
+
+ token.clone()
+ })
+ .collect();
+
+ shirabe_php_shim::implode(" ", &tokens)
+ }
+}
+
+impl InputInterface for ArgvInput {
+ fn get_first_argument(&self) -> Option<String> {
+ ArgvInput::get_first_argument(self)
+ }
+
+ fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool {
+ ArgvInput::has_parameter_option(self, values, only_params)
+ }
+
+ fn get_parameter_option(
+ &self,
+ values: PhpMixed,
+ default: PhpMixed,
+ only_params: bool,
+ ) -> PhpMixed {
+ ArgvInput::get_parameter_option(self, values, default, only_params)
+ }
+
+ fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> {
+ ArgvInput::bind(self, definition)
+ }
+
+ fn validate(&mut self) -> anyhow::Result<()> {
+ self.inner.validate()
+ }
+
+ fn get_arguments(&self) -> IndexMap<String, PhpMixed> {
+ self.inner.get_arguments()
+ }
+
+ fn get_argument(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ 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<String, PhpMixed> {
+ self.inner.get_options()
+ }
+
+ fn get_option(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ 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)
+ }
+}
+
+impl StreamableInputInterface for ArgvInput {
+ fn set_stream(&mut self, stream: PhpMixed) {
+ self.inner.set_stream(stream)
+ }
+
+ fn get_stream(&self) -> Option<PhpMixed> {
+ self.inner.get_stream()
+ }
+}
+
+/// PHP `(array) $values` cast: a string becomes a single-element array.
+fn to_array(values: PhpMixed) -> Vec<String> {
+ match values {
+ PhpMixed::List(list) => list
+ .into_iter()
+ .map(|v| shirabe_php_shim::php_to_string(&v))
+ .collect(),
+ PhpMixed::Array(array) => array
+ .into_iter()
+ .map(|(_, v)| shirabe_php_shim::php_to_string(&v))
+ .collect(),
+ PhpMixed::Null => vec![],
+ other => vec![shirabe_php_shim::php_to_string(&other)],
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs b/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs
index 0c68cf2..17c8aa8 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs
@@ -1,69 +1,378 @@
-use crate::symfony::console::input::InputDefinition;
-use crate::symfony::console::input::InputInterface;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::exception::invalid_option_exception::InvalidOptionException;
+use crate::symfony::console::input::input::Input;
+use crate::symfony::console::input::input_definition::InputDefinition;
+use crate::symfony::console::input::input_interface::InputInterface;
use indexmap::IndexMap;
use shirabe_php_shim::PhpMixed;
+/// ArrayInput represents an input provided as an array.
+///
+/// Usage:
+///
+/// $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']);
+///
+/// PHP arrays can mix integer and string keys; `parameters` preserves both the
+/// key type (`PhpMixed::Int` / `PhpMixed::String`) and the insertion order.
#[derive(Debug)]
-pub struct ArrayInput;
+pub struct ArrayInput {
+ pub(crate) inner: Input,
+ parameters: Vec<(PhpMixed, PhpMixed)>,
+}
impl ArrayInput {
pub fn new(
- _parameters: IndexMap<String, PhpMixed>,
- _definition: Option<InputDefinition>,
- ) -> Self {
- todo!()
+ parameters: Vec<(PhpMixed, PhpMixed)>,
+ definition: Option<InputDefinition>,
+ ) -> anyhow::Result<Self> {
+ let mut array_input = ArrayInput {
+ inner: Input::new(None)?,
+ parameters,
+ };
+
+ // parent::__construct($definition)
+ match definition {
+ None => {}
+ Some(definition) => {
+ array_input.bind(&definition)?;
+ array_input.inner.validate()?;
+ }
+ }
+
+ Ok(array_input)
+ }
+
+ pub fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> {
+ self.inner.arguments = IndexMap::new();
+ self.inner.options = IndexMap::new();
+ self.inner.definition = definition.clone();
+
+ self.parse()?;
+
+ Ok(())
+ }
+
+ pub fn get_first_argument(&self) -> Option<PhpMixed> {
+ for (param, value) in &self.parameters {
+ // $param && \is_string($param) && '-' === $param[0]
+ if let PhpMixed::String(param) = param {
+ if !param.is_empty() && param.as_bytes()[0] == b'-' {
+ continue;
+ }
+ }
+
+ return Some(value.clone());
+ }
+
+ None
+ }
+
+ pub fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool {
+ let values = to_array(values);
+
+ for (k, v) in &self.parameters {
+ // if (!\is_int($k)) { $v = $k; }
+ let v: PhpMixed = match k {
+ PhpMixed::Int(_) => v.clone(),
+ _ => k.clone(),
+ };
+
+ if only_params && matches!(&v, PhpMixed::String(s) if s == "--") {
+ return false;
+ }
+
+ if values.iter().any(|x| x == &v) {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ pub fn get_parameter_option(
+ &self,
+ values: PhpMixed,
+ default: PhpMixed,
+ only_params: bool,
+ ) -> PhpMixed {
+ let values = to_array(values);
+
+ for (k, v) in &self.parameters {
+ // $onlyParams && ('--' === $k || (\is_int($k) && '--' === $v))
+ if only_params {
+ let k_is_double_dash = matches!(k, PhpMixed::String(s) if s == "--");
+ let int_v_double_dash =
+ matches!(k, PhpMixed::Int(_)) && matches!(v, PhpMixed::String(s) if s == "--");
+ if k_is_double_dash || int_v_double_dash {
+ return default;
+ }
+ }
+
+ match k {
+ PhpMixed::Int(_) => {
+ if values.iter().any(|x| x == v) {
+ return PhpMixed::Bool(true);
+ }
+ }
+ _ => {
+ if values.iter().any(|x| x == k) {
+ return v.clone();
+ }
+ }
+ }
+ }
+
+ default
+ }
+
+ /// Returns a stringified representation of the args passed to the command.
+ pub fn to_string(&self) -> String {
+ let mut params: Vec<String> = vec![];
+ for (param, val) in &self.parameters {
+ // $param && \is_string($param) && '-' === $param[0]
+ let is_option_key =
+ matches!(param, PhpMixed::String(s) if !s.is_empty() && s.as_bytes()[0] == b'-');
+ if is_option_key {
+ let param = param.as_string().unwrap();
+ let glue = if param.as_bytes().get(1) == Some(&b'-') {
+ "="
+ } else {
+ " "
+ };
+ if let PhpMixed::List(list) = val {
+ for v in list {
+ let v = shirabe_php_shim::php_to_string(v);
+ params.push(format!(
+ "{}{}",
+ param,
+ if v != "" {
+ format!("{}{}", glue, self.inner.escape_token(&v))
+ } else {
+ String::new()
+ }
+ ));
+ }
+ } else {
+ let val = shirabe_php_shim::php_to_string(val);
+ params.push(format!(
+ "{}{}",
+ param,
+ if val != "" {
+ format!("{}{}", glue, self.inner.escape_token(&val))
+ } else {
+ String::new()
+ }
+ ));
+ }
+ } else if let PhpMixed::List(list) = val {
+ let escaped: Vec<String> = list
+ .iter()
+ .map(|v| self.inner.escape_token(&shirabe_php_shim::php_to_string(v)))
+ .collect();
+ params.push(shirabe_php_shim::implode(" ", &escaped));
+ } else {
+ params.push(
+ self.inner
+ .escape_token(&shirabe_php_shim::php_to_string(val)),
+ );
+ }
+ }
+
+ shirabe_php_shim::implode(" ", &params)
+ }
+
+ fn parse(&mut self) -> anyhow::Result<()> {
+ // Clone to avoid borrowing self while mutating; PHP iterates over a copy semantically.
+ let parameters = self.parameters.clone();
+ for (key, value) in parameters {
+ let key = shirabe_php_shim::php_to_string(&key);
+ if key == "--" {
+ return Ok(());
+ }
+ if shirabe_php_shim::str_starts_with(&key, "--") {
+ self.add_long_option(&shirabe_php_shim::substr(&key, 2, None), value)?;
+ } else if shirabe_php_shim::str_starts_with(&key, "-") {
+ self.add_short_option(&shirabe_php_shim::substr(&key, 1, None), value)?;
+ } else {
+ self.add_argument(&PhpMixed::String(key), value)?;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Adds a short option value.
+ fn add_short_option(&mut self, shortcut: &str, value: PhpMixed) -> anyhow::Result<()> {
+ if !self.inner.definition.has_shortcut(shortcut) {
+ return Err(InvalidOptionException(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"-%s\" option does not exist.",
+ &[PhpMixed::String(shortcut.to_string())],
+ ),
+ code: 0,
+ },
+ ))
+ .into());
+ }
+
+ self.add_long_option(
+ &self
+ .inner
+ .definition
+ .get_option_for_shortcut(shortcut)?
+ .get_name()
+ .to_string(),
+ value,
+ )
+ }
+
+ /// Adds a long option value.
+ fn add_long_option(&mut self, name: &str, mut value: PhpMixed) -> anyhow::Result<()> {
+ if !self.inner.definition.has_option(name) {
+ if !self.inner.definition.has_negation(name) {
+ return Err(InvalidOptionException(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option does not exist.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ },
+ ))
+ .into());
+ }
+
+ let option_name = self.inner.definition.negation_to_name(name)?;
+ self.inner
+ .options
+ .insert(option_name, PhpMixed::Bool(false));
+
+ return Ok(());
+ }
+
+ let option = self.inner.definition.get_option(name)?;
+
+ if matches!(value, PhpMixed::Null) {
+ if option.is_value_required() {
+ return Err(InvalidOptionException(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option requires a value.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ },
+ ))
+ .into());
+ }
+
+ if !option.is_value_optional() {
+ value = PhpMixed::Bool(true);
+ }
+ }
+
+ self.inner.options.insert(name.to_string(), value);
+
+ Ok(())
+ }
+
+ /// Adds an argument value.
+ fn add_argument(&mut self, name: &PhpMixed, value: PhpMixed) -> anyhow::Result<()> {
+ if !self.inner.definition.has_argument(name) {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"%s\" argument does not exist.",
+ &[name.clone()],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ self.inner
+ .arguments
+ .insert(shirabe_php_shim::php_to_string(name), value);
+
+ Ok(())
}
}
impl InputInterface for ArrayInput {
fn get_first_argument(&self) -> Option<String> {
- todo!()
+ ArrayInput::get_first_argument(self).map(|v| shirabe_php_shim::php_to_string(&v))
}
- fn has_parameter_option(&self, _values: &[&str], _only_params: bool) -> bool {
- todo!()
+
+ fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool {
+ ArrayInput::has_parameter_option(self, values, only_params)
}
+
fn get_parameter_option(
&self,
- _values: &[&str],
- _default: PhpMixed,
- _only_params: bool,
+ values: PhpMixed,
+ default: PhpMixed,
+ only_params: bool,
) -> PhpMixed {
- todo!()
+ ArrayInput::get_parameter_option(self, values, default, only_params)
}
- fn bind(&mut self, _definition: &InputDefinition) -> anyhow::Result<()> {
- todo!()
+
+ fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> {
+ ArrayInput::bind(self, definition)
}
- fn validate(&self) -> anyhow::Result<()> {
- todo!()
+
+ fn validate(&mut self) -> anyhow::Result<()> {
+ self.inner.validate()
}
+
fn get_arguments(&self) -> IndexMap<String, PhpMixed> {
- todo!()
+ self.inner.get_arguments()
}
- fn get_argument(&self, _name: &str) -> PhpMixed {
- todo!()
+
+ fn get_argument(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ self.inner.get_argument(name)
}
- fn set_argument(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
- todo!()
+
+ fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> {
+ self.inner.set_argument(name, value)
}
- fn has_argument(&self, _name: &str) -> bool {
- todo!()
+
+ fn has_argument(&self, name: &str) -> bool {
+ self.inner.has_argument(name)
}
+
fn get_options(&self) -> IndexMap<String, PhpMixed> {
- todo!()
+ self.inner.get_options()
}
- fn get_option(&self, _name: &str) -> PhpMixed {
- todo!()
+
+ fn get_option(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ self.inner.get_option(name)
}
- fn set_option(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
- todo!()
+
+ fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> {
+ self.inner.set_option(name, value)
}
- fn has_option(&self, _name: &str) -> bool {
- todo!()
+
+ fn has_option(&self, name: &str) -> bool {
+ self.inner.has_option(name)
}
+
fn is_interactive(&self) -> bool {
- todo!()
+ self.inner.is_interactive()
}
- fn set_interactive(&mut self, _interactive: bool) {
- todo!()
+
+ fn set_interactive(&mut self, interactive: bool) {
+ self.inner.set_interactive(interactive)
+ }
+}
+
+/// PHP `(array) $values` cast: a string becomes a single-element array.
+fn to_array(values: PhpMixed) -> Vec<PhpMixed> {
+ match values {
+ PhpMixed::List(list) => list.into_iter().map(|v| *v).collect(),
+ PhpMixed::Array(array) => array.into_iter().map(|(_, v)| *v).collect(),
+ PhpMixed::Null => vec![],
+ other => vec![other],
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input.rs b/crates/shirabe-external-packages/src/symfony/console/input/input.rs
new file mode 100644
index 0000000..4c878bc
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/input/input.rs
@@ -0,0 +1,252 @@
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::exception::runtime_exception::RuntimeException;
+use crate::symfony::console::input::input_definition::InputDefinition;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+/// Input is the base class for all concrete Input classes.
+///
+/// Three concrete classes are provided by default:
+///
+/// * `ArgvInput`: The input comes from the CLI arguments (argv)
+/// * `StringInput`: The input is provided as a string
+/// * `ArrayInput`: The input is provided as an array
+#[derive(Debug)]
+pub struct Input {
+ pub(crate) definition: InputDefinition,
+ pub(crate) stream: PhpMixed,
+ pub(crate) options: IndexMap<String, PhpMixed>,
+ pub(crate) arguments: IndexMap<String, PhpMixed>,
+ pub(crate) interactive: bool,
+}
+
+impl Input {
+ pub fn new(definition: Option<InputDefinition>) -> anyhow::Result<Self> {
+ let mut input = Input {
+ definition: InputDefinition::new(vec![])?,
+ stream: PhpMixed::Null,
+ options: IndexMap::new(),
+ arguments: IndexMap::new(),
+ interactive: true,
+ };
+
+ match definition {
+ None => {
+ input.definition = InputDefinition::new(vec![])?;
+ }
+ Some(definition) => {
+ input.bind(&definition)?;
+ input.validate()?;
+ }
+ }
+
+ Ok(input)
+ }
+
+ pub fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> {
+ self.arguments = IndexMap::new();
+ self.options = IndexMap::new();
+ self.definition = definition.clone();
+
+ self.parse()?;
+
+ Ok(())
+ }
+
+ /// Processes command line arguments.
+ ///
+ /// This is abstract in PHP; concrete subclasses provide their own `parse`.
+ /// Since `Input` is embedded via `inner` in the subclasses, the subclass
+ /// drives the parsing instead.
+ fn parse(&mut self) -> anyhow::Result<()> {
+ unreachable!("Input::parse is abstract and overridden by subclasses")
+ }
+
+ pub fn validate(&mut self) -> anyhow::Result<()> {
+ let definition = &self.definition;
+ let given_arguments = &self.arguments;
+
+ let missing_arguments: Vec<String> = shirabe_php_shim::array_filter(
+ &shirabe_php_shim::array_keys(definition.get_arguments()),
+ |argument: &String| {
+ !given_arguments.contains_key(argument)
+ && definition
+ .get_argument(&PhpMixed::String(argument.clone()))
+ .map(|a| a.is_required())
+ .unwrap_or(false)
+ },
+ );
+
+ if missing_arguments.len() > 0 {
+ return Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "Not enough arguments (missing: \"%s\").",
+ &[PhpMixed::String(shirabe_php_shim::implode(
+ ", ",
+ &missing_arguments,
+ ))],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ Ok(())
+ }
+
+ pub fn is_interactive(&self) -> bool {
+ self.interactive
+ }
+
+ pub fn set_interactive(&mut self, interactive: bool) {
+ self.interactive = interactive;
+ }
+
+ pub fn get_arguments(&self) -> IndexMap<String, PhpMixed> {
+ shirabe_php_shim::array_merge_map(
+ self.definition.get_argument_defaults(),
+ self.arguments.clone(),
+ )
+ }
+
+ pub fn get_argument(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ if !self
+ .definition
+ .has_argument(&PhpMixed::String(name.to_string()))
+ {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"%s\" argument does not exist.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ Ok(match self.arguments.get(name) {
+ Some(value) => value.clone(),
+ None => self
+ .definition
+ .get_argument(&PhpMixed::String(name.to_string()))?
+ .get_default()
+ .clone(),
+ })
+ }
+
+ pub fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> {
+ if !self
+ .definition
+ .has_argument(&PhpMixed::String(name.to_string()))
+ {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"%s\" argument does not exist.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ self.arguments.insert(name.to_string(), value);
+
+ Ok(())
+ }
+
+ pub fn has_argument(&self, name: &str) -> bool {
+ self.definition
+ .has_argument(&PhpMixed::String(name.to_string()))
+ }
+
+ pub fn get_options(&self) -> IndexMap<String, PhpMixed> {
+ shirabe_php_shim::array_merge_map(
+ self.definition.get_option_defaults(),
+ self.options.clone(),
+ )
+ }
+
+ pub fn get_option(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ if self.definition.has_negation(name) {
+ let value = self.get_option(&self.definition.negation_to_name(name)?)?;
+ if matches!(value, PhpMixed::Null) {
+ return Ok(value);
+ }
+
+ return Ok(PhpMixed::Bool(!value.as_bool().unwrap_or(false)));
+ }
+
+ if !self.definition.has_option(name) {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"%s\" option does not exist.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ Ok(if self.options.contains_key(name) {
+ self.options[name].clone()
+ } else {
+ self.definition.get_option(name)?.get_default().clone()
+ })
+ }
+
+ pub fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> {
+ if self.definition.has_negation(name) {
+ let negated = self.definition.negation_to_name(name)?;
+ self.options
+ .insert(negated, PhpMixed::Bool(!value.as_bool().unwrap_or(false)));
+
+ return Ok(());
+ } else if !self.definition.has_option(name) {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"%s\" option does not exist.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ self.options.insert(name.to_string(), value);
+
+ Ok(())
+ }
+
+ pub fn has_option(&self, name: &str) -> bool {
+ self.definition.has_option(name) || self.definition.has_negation(name)
+ }
+
+ /// Escapes a token through escapeshellarg if it contains unsafe chars.
+ pub fn escape_token(&self, token: &str) -> String {
+ let mut matches: Vec<Option<String>> = vec![];
+ if shirabe_php_shim::preg_match("{^[\\w-]+$}", token, &mut matches) != 0 {
+ token.to_string()
+ } else {
+ shirabe_php_shim::escapeshellarg(token)
+ }
+ }
+
+ pub fn set_stream(&mut self, stream: PhpMixed) {
+ self.stream = stream;
+ }
+
+ pub fn get_stream(&self) -> Option<PhpMixed> {
+ match &self.stream {
+ PhpMixed::Null => None,
+ other => Some(other.clone()),
+ }
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs
index 84b722d..e320878 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs
@@ -1,7 +1,14 @@
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::exception::logic_exception::LogicException;
use shirabe_php_shim::PhpMixed;
#[derive(Debug)]
-pub struct InputArgument;
+pub struct InputArgument {
+ name: String,
+ mode: i64,
+ default: PhpMixed,
+ description: String,
+}
impl InputArgument {
pub const REQUIRED: i64 = 1;
@@ -9,27 +16,85 @@ impl InputArgument {
pub const IS_ARRAY: i64 = 4;
pub fn new(
- _name: &str,
- _mode: Option<i64>,
- _description: &str,
- _default: Option<PhpMixed>,
- ) -> Self {
- todo!()
+ name: String,
+ mode: Option<i64>,
+ description: String,
+ default: PhpMixed,
+ ) -> anyhow::Result<Self> {
+ let mode = match mode {
+ None => Self::OPTIONAL,
+ Some(m) if m > 7 || m < 1 => {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: format!("Argument mode \"{}\" is not valid.", m),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+ Some(m) => m,
+ };
+
+ let mut argument = InputArgument {
+ name,
+ mode,
+ description,
+ default: PhpMixed::Null,
+ };
+
+ argument.set_default(default)?;
+
+ Ok(argument)
}
- pub fn get_name(&self) -> String {
- todo!()
+ pub fn get_name(&self) -> &str {
+ &self.name
}
pub fn is_required(&self) -> bool {
- todo!()
+ Self::REQUIRED == (Self::REQUIRED & self.mode)
}
pub fn is_array(&self) -> bool {
- todo!()
+ Self::IS_ARRAY == (Self::IS_ARRAY & self.mode)
+ }
+
+ pub fn set_default(&mut self, default: PhpMixed) -> anyhow::Result<()> {
+ if self.is_required() && !matches!(default, PhpMixed::Null) {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: "Cannot set a default value except for InputArgument::OPTIONAL mode."
+ .to_string(),
+ code: 0,
+ })
+ .into());
+ }
+
+ let default = if self.is_array() {
+ match default {
+ PhpMixed::Null => PhpMixed::List(vec![]),
+ PhpMixed::List(_) => default,
+ _ => {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: "A default value for an array argument must be an array."
+ .to_string(),
+ code: 0,
+ })
+ .into());
+ }
+ }
+ } else {
+ default
+ };
+
+ self.default = default;
+ Ok(())
+ }
+
+ pub fn get_default(&self) -> &PhpMixed {
+ &self.default
}
- pub fn get_default(&self) -> Option<PhpMixed> {
- todo!()
+ pub fn get_description(&self) -> &str {
+ &self.description
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_aware_interface.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_aware_interface.rs
new file mode 100644
index 0000000..ae46465
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/input/input_aware_interface.rs
@@ -0,0 +1,5 @@
+use crate::symfony::console::input::input_interface::InputInterface;
+
+pub trait InputAwareInterface {
+ fn set_input(&mut self, input: Box<dyn InputInterface>);
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs
index f2d0318..5a3eef5 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs
@@ -1,32 +1,472 @@
-use crate::symfony::console::input::InputArgument;
-use crate::symfony::console::input::InputOption;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::exception::logic_exception::LogicException;
+use crate::symfony::console::input::input_argument::InputArgument;
+use crate::symfony::console::input::input_option::InputOption;
+use indexmap::IndexMap;
use shirabe_php_shim::PhpMixed;
+use std::rc::Rc;
+/// A InputDefinition represents a set of valid command line arguments and options.
+///
+/// `InputArgument` and `InputOption` are stored behind `Rc` to model PHP's
+/// shared object references; this also lets the definition be cloned cheaply
+/// (PHP `bind` assigns the definition by reference).
+#[derive(Debug, Clone)]
+pub struct InputDefinition {
+ arguments: IndexMap<String, Rc<InputArgument>>,
+ required_count: i64,
+ last_array_argument: Option<Rc<InputArgument>>,
+ last_optional_argument: Option<Rc<InputArgument>>,
+ options: IndexMap<String, Rc<InputOption>>,
+ negations: IndexMap<String, String>,
+ shortcuts: IndexMap<String, String>,
+}
+
+/// A definition entry is either an InputArgument or an InputOption.
#[derive(Debug)]
-pub struct InputDefinition;
+pub enum DefinitionItem {
+ InputArgument(InputArgument),
+ InputOption(InputOption),
+}
impl InputDefinition {
- pub fn new(_definition: Vec<PhpMixed>) -> Self {
- todo!()
+ pub fn new(definition: Vec<DefinitionItem>) -> anyhow::Result<Self> {
+ let mut input_definition = InputDefinition {
+ arguments: IndexMap::new(),
+ required_count: 0,
+ last_array_argument: None,
+ last_optional_argument: None,
+ options: IndexMap::new(),
+ negations: IndexMap::new(),
+ shortcuts: IndexMap::new(),
+ };
+ input_definition.set_definition(definition)?;
+ Ok(input_definition)
+ }
+
+ /// Sets the definition of the input.
+ pub fn set_definition(&mut self, definition: Vec<DefinitionItem>) -> anyhow::Result<()> {
+ let mut arguments = vec![];
+ let mut options = vec![];
+ for item in definition {
+ match item {
+ DefinitionItem::InputOption(option) => {
+ options.push(option);
+ }
+ DefinitionItem::InputArgument(argument) => {
+ arguments.push(argument);
+ }
+ }
+ }
+
+ self.set_arguments(arguments)?;
+ self.set_options(options)?;
+
+ Ok(())
+ }
+
+ /// Sets the InputArgument objects.
+ pub fn set_arguments(&mut self, arguments: Vec<InputArgument>) -> anyhow::Result<()> {
+ self.arguments = IndexMap::new();
+ self.required_count = 0;
+ self.last_optional_argument = None;
+ self.last_array_argument = None;
+ self.add_arguments(Some(arguments))?;
+ Ok(())
+ }
+
+ /// Adds an array of InputArgument objects.
+ pub fn add_arguments(&mut self, arguments: Option<Vec<InputArgument>>) -> anyhow::Result<()> {
+ if let Some(arguments) = arguments {
+ for argument in arguments {
+ self.add_argument(argument)?;
+ }
+ }
+ Ok(())
+ }
+
+ pub fn add_argument(&mut self, argument: InputArgument) -> anyhow::Result<()> {
+ let argument = Rc::new(argument);
+
+ if self.arguments.contains_key(argument.get_name()) {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: shirabe_php_shim::sprintf(
+ "An argument with name \"%s\" already exists.",
+ &[PhpMixed::String(argument.get_name().to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ if let Some(last_array_argument) = &self.last_array_argument {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: shirabe_php_shim::sprintf(
+ "Cannot add a required argument \"%s\" after an array argument \"%s\".",
+ &[
+ PhpMixed::String(argument.get_name().to_string()),
+ PhpMixed::String(last_array_argument.get_name().to_string()),
+ ],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ if argument.is_required() {
+ if let Some(last_optional_argument) = &self.last_optional_argument {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: shirabe_php_shim::sprintf(
+ "Cannot add a required argument \"%s\" after an optional one \"%s\".",
+ &[
+ PhpMixed::String(argument.get_name().to_string()),
+ PhpMixed::String(last_optional_argument.get_name().to_string()),
+ ],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+ }
+
+ if argument.is_array() {
+ self.last_array_argument = Some(Rc::clone(&argument));
+ }
+
+ if argument.is_required() {
+ self.required_count += 1;
+ } else {
+ self.last_optional_argument = Some(Rc::clone(&argument));
+ }
+
+ self.arguments
+ .insert(argument.get_name().to_string(), argument);
+
+ Ok(())
+ }
+
+ /// Returns an InputArgument by name or by position.
+ pub fn get_argument(&self, name: &PhpMixed) -> anyhow::Result<Rc<InputArgument>> {
+ if !self.has_argument(name) {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"%s\" argument does not exist.",
+ &[name.clone()],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ match name {
+ PhpMixed::Int(index) => {
+ let arguments: Vec<Rc<InputArgument>> = self.arguments.values().cloned().collect();
+ Ok(Rc::clone(&arguments[*index as usize]))
+ }
+ _ => {
+ let key = shirabe_php_shim::php_to_string(name);
+ Ok(Rc::clone(&self.arguments[&key]))
+ }
+ }
+ }
+
+ /// Returns true if an InputArgument object exists by name or position.
+ pub fn has_argument(&self, name: &PhpMixed) -> bool {
+ match name {
+ PhpMixed::Int(index) => {
+ let arguments: Vec<Rc<InputArgument>> = self.arguments.values().cloned().collect();
+ *index >= 0 && (*index as usize) < arguments.len()
+ }
+ _ => {
+ let key = shirabe_php_shim::php_to_string(name);
+ self.arguments.contains_key(&key)
+ }
+ }
+ }
+
+ /// Gets the array of InputArgument objects.
+ pub fn get_arguments(&self) -> &IndexMap<String, Rc<InputArgument>> {
+ &self.arguments
+ }
+
+ /// Returns the number of InputArguments.
+ pub fn get_argument_count(&self) -> i64 {
+ if self.last_array_argument.is_some() {
+ i64::MAX
+ } else {
+ self.arguments.len() as i64
+ }
+ }
+
+ /// Returns the number of required InputArguments.
+ pub fn get_argument_required_count(&self) -> i64 {
+ self.required_count
+ }
+
+ pub fn get_argument_defaults(&self) -> IndexMap<String, PhpMixed> {
+ let mut values = IndexMap::new();
+ for argument in self.arguments.values() {
+ values.insert(
+ argument.get_name().to_string(),
+ argument.get_default().clone(),
+ );
+ }
+
+ values
+ }
+
+ /// Sets the InputOption objects.
+ pub fn set_options(&mut self, options: Vec<InputOption>) -> anyhow::Result<()> {
+ self.options = IndexMap::new();
+ self.shortcuts = IndexMap::new();
+ self.negations = IndexMap::new();
+ self.add_options(options)?;
+ Ok(())
+ }
+
+ /// Adds an array of InputOption objects.
+ pub fn add_options(&mut self, options: Vec<InputOption>) -> anyhow::Result<()> {
+ for option in options {
+ self.add_option(option)?;
+ }
+ Ok(())
+ }
+
+ pub fn add_option(&mut self, option: InputOption) -> anyhow::Result<()> {
+ let option = Rc::new(option);
+
+ if let Some(existing) = self.options.get(option.get_name()) {
+ if !option.equals(existing) {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: shirabe_php_shim::sprintf(
+ "An option named \"%s\" already exists.",
+ &[PhpMixed::String(option.get_name().to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+ }
+ if self.negations.contains_key(option.get_name()) {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: shirabe_php_shim::sprintf(
+ "An option named \"%s\" already exists.",
+ &[PhpMixed::String(option.get_name().to_string())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ if let Some(shortcut) = option.get_shortcut() {
+ for shortcut in shirabe_php_shim::explode("|", shortcut) {
+ if let Some(existing_name) = self.shortcuts.get(&shortcut) {
+ if !option.equals(&self.options[existing_name]) {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: shirabe_php_shim::sprintf(
+ "An option with shortcut \"%s\" already exists.",
+ &[PhpMixed::String(shortcut.clone())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+ }
+ }
+ }
+
+ self.options
+ .insert(option.get_name().to_string(), Rc::clone(&option));
+ if let Some(shortcut) = option.get_shortcut() {
+ for shortcut in shirabe_php_shim::explode("|", shortcut) {
+ self.shortcuts
+ .insert(shortcut, option.get_name().to_string());
+ }
+ }
+
+ if option.is_negatable() {
+ let negated_name = format!("no-{}", option.get_name());
+ if self.options.contains_key(&negated_name) {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: shirabe_php_shim::sprintf(
+ "An option named \"%s\" already exists.",
+ &[PhpMixed::String(negated_name.clone())],
+ ),
+ code: 0,
+ })
+ .into());
+ }
+ self.negations
+ .insert(negated_name, option.get_name().to_string());
+ }
+
+ Ok(())
+ }
+
+ /// Returns an InputOption by name.
+ pub fn get_option(&self, name: &str) -> anyhow::Result<Rc<InputOption>> {
+ if !self.has_option(name) {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option does not exist.",
+ &[PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ Ok(Rc::clone(&self.options[name]))
}
- pub fn add_argument(&mut self, _argument: InputArgument) {
- todo!()
+ /// Returns true if an InputOption object exists by name.
+ ///
+ /// This method can't be used to check if the user included the option when
+ /// executing the command (use getOption() instead).
+ pub fn has_option(&self, name: &str) -> bool {
+ self.options.contains_key(name)
}
- pub fn add_option(&mut self, _option: InputOption) {
- todo!()
+ /// Gets the array of InputOption objects.
+ pub fn get_options(&self) -> &IndexMap<String, Rc<InputOption>> {
+ &self.options
}
- pub fn has_option(&self, _name: &str) -> bool {
- todo!()
+ /// Returns true if an InputOption object exists by shortcut.
+ pub fn has_shortcut(&self, name: &str) -> bool {
+ self.shortcuts.contains_key(name)
}
- pub fn get_option(&self, _name: &str) -> anyhow::Result<PhpMixed> {
- todo!()
+ /// Returns true if an InputOption object exists by negated name.
+ pub fn has_negation(&self, name: &str) -> bool {
+ self.negations.contains_key(name)
}
- pub fn has_argument(&self, _name: &str) -> bool {
- todo!()
+ /// Gets an InputOption by shortcut.
+ pub fn get_option_for_shortcut(&self, shortcut: &str) -> anyhow::Result<Rc<InputOption>> {
+ self.get_option(&self.shortcut_to_name(shortcut)?)
+ }
+
+ pub fn get_option_defaults(&self) -> IndexMap<String, PhpMixed> {
+ let mut values = IndexMap::new();
+ for option in self.options.values() {
+ values.insert(option.get_name().to_string(), option.get_default().clone());
+ }
+
+ values
+ }
+
+ /// Returns the InputOption name given a shortcut.
+ pub fn shortcut_to_name(&self, shortcut: &str) -> anyhow::Result<String> {
+ match self.shortcuts.get(shortcut) {
+ None => Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"-%s\" option does not exist.",
+ &[PhpMixed::String(shortcut.to_string())],
+ ),
+ code: 0,
+ })
+ .into(),
+ ),
+ Some(name) => Ok(name.clone()),
+ }
+ }
+
+ /// Returns the InputOption name given a negation.
+ pub fn negation_to_name(&self, negation: &str) -> anyhow::Result<String> {
+ match self.negations.get(negation) {
+ None => Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The \"--%s\" option does not exist.",
+ &[PhpMixed::String(negation.to_string())],
+ ),
+ code: 0,
+ })
+ .into(),
+ ),
+ Some(name) => Ok(name.clone()),
+ }
+ }
+
+ /// Gets the synopsis.
+ pub fn get_synopsis(&self, short: bool) -> String {
+ let mut elements: Vec<String> = vec![];
+
+ if short && !self.get_options().is_empty() {
+ elements.push("[options]".to_string());
+ } else if !short {
+ for option in self.get_options().values() {
+ let mut value = String::new();
+ if option.accept_value() {
+ value = shirabe_php_shim::sprintf(
+ " %s%s%s",
+ &[
+ PhpMixed::String(if option.is_value_optional() {
+ "[".to_string()
+ } else {
+ String::new()
+ }),
+ PhpMixed::String(shirabe_php_shim::strtoupper(option.get_name())),
+ PhpMixed::String(if option.is_value_optional() {
+ "]".to_string()
+ } else {
+ String::new()
+ }),
+ ],
+ );
+ }
+
+ let shortcut = match option.get_shortcut() {
+ Some(shortcut) => {
+ shirabe_php_shim::sprintf("-%s|", &[PhpMixed::String(shortcut.to_string())])
+ }
+ None => String::new(),
+ };
+ let negation = if option.is_negatable() {
+ shirabe_php_shim::sprintf(
+ "|--no-%s",
+ &[PhpMixed::String(option.get_name().to_string())],
+ )
+ } else {
+ String::new()
+ };
+ elements.push(shirabe_php_shim::sprintf(
+ "[%s--%s%s%s]",
+ &[
+ PhpMixed::String(shortcut),
+ PhpMixed::String(option.get_name().to_string()),
+ PhpMixed::String(value),
+ PhpMixed::String(negation),
+ ],
+ ));
+ }
+ }
+
+ if elements.len() > 0 && !self.get_arguments().is_empty() {
+ elements.push("[--]".to_string());
+ }
+
+ let mut tail = String::new();
+ for argument in self.get_arguments().values() {
+ let mut element = format!("<{}>", argument.get_name());
+ if argument.is_array() {
+ element.push_str("...");
+ }
+
+ if !argument.is_required() {
+ element = format!("[{}", element);
+ tail.push(']');
+ }
+
+ elements.push(element);
+ }
+
+ format!("{}{}", shirabe_php_shim::implode(" ", &elements), tail)
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_interface.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_interface.rs
index 14458e4..d271845 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/input_interface.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/input_interface.rs
@@ -1,32 +1,39 @@
+use crate::symfony::console::input::input_definition::InputDefinition;
use shirabe_php_shim::PhpMixed;
-pub trait InputInterface: std::fmt::Debug {
+pub trait InputInterface: std::fmt::Debug + shirabe_php_shim::AsAny {
fn get_first_argument(&self) -> Option<String>;
- fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool;
+
+ fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool;
+
fn get_parameter_option(
&self,
- values: &[&str],
+ values: PhpMixed,
default: PhpMixed,
only_params: bool,
) -> PhpMixed;
- fn bind(
- &mut self,
- definition: &crate::symfony::console::input::InputDefinition,
- ) -> anyhow::Result<()>;
- fn validate(&self) -> anyhow::Result<()>;
+
+ fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()>;
+
+ fn validate(&mut self) -> anyhow::Result<()>;
+
fn get_arguments(&self) -> indexmap::IndexMap<String, PhpMixed>;
- fn get_argument(&self, name: &str) -> PhpMixed;
+
+ fn get_argument(&self, name: &str) -> anyhow::Result<PhpMixed>;
+
fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>;
+
fn has_argument(&self, name: &str) -> bool;
+
fn get_options(&self) -> indexmap::IndexMap<String, PhpMixed>;
- fn get_option(&self, name: &str) -> PhpMixed;
+
+ fn get_option(&self, name: &str) -> anyhow::Result<PhpMixed>;
+
fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>;
+
fn has_option(&self, name: &str) -> bool;
+
fn is_interactive(&self) -> bool;
- fn set_interactive(&mut self, interactive: bool);
- /// Equivalent to PHP `(string) $input` (Input::__toString).
- fn to_input_string(&self) -> String {
- todo!()
- }
+ fn set_interactive(&mut self, interactive: bool);
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs
index 673ca07..c929d03 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs
@@ -1,7 +1,15 @@
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::exception::logic_exception::LogicException;
use shirabe_php_shim::PhpMixed;
#[derive(Debug)]
-pub struct InputOption;
+pub struct InputOption {
+ name: String,
+ shortcut: Option<String>,
+ mode: i64,
+ default: PhpMixed,
+ description: String,
+}
impl InputOption {
pub const VALUE_NONE: i64 = 1;
@@ -11,36 +19,190 @@ impl InputOption {
pub const VALUE_NEGATABLE: i64 = 16;
pub fn new(
- _name: &str,
- _shortcut: Option<&str>,
- _mode: Option<i64>,
- _description: &str,
- _default: PhpMixed,
- ) -> Self {
- todo!()
+ name: &str,
+ shortcut: PhpMixed,
+ mode: Option<i64>,
+ description: String,
+ default: PhpMixed,
+ ) -> anyhow::Result<Self> {
+ let name = if name.starts_with("--") {
+ name[2..].to_string()
+ } else {
+ name.to_string()
+ };
+
+ if name.is_empty() {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: "An option name cannot be empty.".to_string(),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+
+ let shortcut = match shortcut {
+ PhpMixed::String(ref s) if s.is_empty() => None,
+ PhpMixed::List(ref v) if v.is_empty() => None,
+ PhpMixed::Bool(false) => None,
+ PhpMixed::Null => None,
+ PhpMixed::List(ref arr) => {
+ let parts: Vec<String> = arr
+ .iter()
+ .filter_map(|v| {
+ if let PhpMixed::String(s) = v.as_ref() {
+ Some(s.clone())
+ } else {
+ None
+ }
+ })
+ .collect();
+ let joined = shirabe_php_shim::implode("|", &parts);
+ Self::normalize_shortcut(joined)?
+ }
+ PhpMixed::String(s) => Self::normalize_shortcut(s)?,
+ _ => None,
+ };
+
+ let mode = match mode {
+ None => Self::VALUE_NONE,
+ Some(m) if m >= (Self::VALUE_NEGATABLE << 1) || m < 1 => {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: format!("Option mode \"{}\" is not valid.", m),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+ Some(m) => m,
+ };
+
+ let mut option = InputOption {
+ name,
+ shortcut,
+ mode,
+ description,
+ default: PhpMixed::Null,
+ };
+
+ if option.is_array() && !option.accept_value() {
+ return Err(InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: "Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.".to_string(),
+ code: 0,
+ })
+ .into());
+ }
+ if option.is_negatable() && option.accept_value() {
+ return Err(InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: "Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.".to_string(),
+ code: 0,
+ })
+ .into());
+ }
+
+ option.set_default(default)?;
+
+ Ok(option)
+ }
+
+ fn normalize_shortcut(s: String) -> anyhow::Result<Option<String>> {
+ let stripped = shirabe_php_shim::ltrim(&s, Some("-"));
+ let parts = shirabe_php_shim::preg_split(r"\|(-?)", &stripped).unwrap_or_default();
+ let filtered: Vec<String> =
+ shirabe_php_shim::array_filter(&parts, |s: &String| !s.is_empty());
+ let result = shirabe_php_shim::implode("|", &filtered);
+ if result.is_empty() {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: "An option shortcut cannot be empty.".to_string(),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+ Ok(Some(result))
+ }
+
+ pub fn get_shortcut(&self) -> Option<&str> {
+ self.shortcut.as_deref()
}
- pub fn get_name(&self) -> String {
- todo!()
+ pub fn get_name(&self) -> &str {
+ &self.name
}
pub fn accept_value(&self) -> bool {
- todo!()
+ self.is_value_required() || self.is_value_optional()
}
pub fn is_value_required(&self) -> bool {
- todo!()
+ Self::VALUE_REQUIRED == (Self::VALUE_REQUIRED & self.mode)
}
pub fn is_value_optional(&self) -> bool {
- todo!()
+ Self::VALUE_OPTIONAL == (Self::VALUE_OPTIONAL & self.mode)
}
pub fn is_array(&self) -> bool {
- todo!()
+ Self::VALUE_IS_ARRAY == (Self::VALUE_IS_ARRAY & self.mode)
+ }
+
+ pub fn is_negatable(&self) -> bool {
+ Self::VALUE_NEGATABLE == (Self::VALUE_NEGATABLE & self.mode)
+ }
+
+ pub fn set_default(&mut self, default: PhpMixed) -> anyhow::Result<()> {
+ if Self::VALUE_NONE == (Self::VALUE_NONE & self.mode) && !matches!(default, PhpMixed::Null)
+ {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: "Cannot set a default value when using InputOption::VALUE_NONE mode."
+ .to_string(),
+ code: 0,
+ })
+ .into());
+ }
+
+ let default = if self.is_array() {
+ match default {
+ PhpMixed::Null => PhpMixed::List(vec![]),
+ PhpMixed::List(_) => default,
+ _ => {
+ return Err(LogicException(shirabe_php_shim::LogicException {
+ message: "A default value for an array option must be an array."
+ .to_string(),
+ code: 0,
+ })
+ .into());
+ }
+ }
+ } else {
+ default
+ };
+
+ self.default = if self.accept_value() || self.is_negatable() {
+ default
+ } else {
+ PhpMixed::Bool(false)
+ };
+ Ok(())
+ }
+
+ pub fn get_default(&self) -> &PhpMixed {
+ &self.default
+ }
+
+ pub fn get_description(&self) -> &str {
+ &self.description
}
- pub fn get_default(&self) -> PhpMixed {
- todo!()
+ pub fn equals(&self, option: &InputOption) -> bool {
+ option.get_name() == self.get_name()
+ && option.get_shortcut() == self.get_shortcut()
+ && option.get_default() == self.get_default()
+ && option.is_negatable() == self.is_negatable()
+ && option.is_array() == self.is_array()
+ && option.is_value_required() == self.is_value_required()
+ && option.is_value_optional() == self.is_value_optional()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/mod.rs b/crates/shirabe-external-packages/src/symfony/console/input/mod.rs
index 8c6a28f..d4813b4 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/mod.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/mod.rs
@@ -1,13 +1,19 @@
+pub mod argv_input;
pub mod array_input;
+pub mod input;
pub mod input_argument;
+pub mod input_aware_interface;
pub mod input_definition;
pub mod input_interface;
pub mod input_option;
pub mod streamable_input_interface;
pub mod string_input;
+pub use argv_input::*;
pub use array_input::*;
+pub use input::*;
pub use input_argument::*;
+pub use input_aware_interface::*;
pub use input_definition::*;
pub use input_interface::*;
pub use input_option::*;
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/streamable_input_interface.rs b/crates/shirabe-external-packages/src/symfony/console/input/streamable_input_interface.rs
index aea2bf2..7279dc4 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/streamable_input_interface.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/streamable_input_interface.rs
@@ -1,7 +1,8 @@
-use crate::symfony::console::input::InputInterface;
+use crate::symfony::console::input::input_interface::InputInterface;
use shirabe_php_shim::PhpMixed;
pub trait StreamableInputInterface: InputInterface {
fn set_stream(&mut self, stream: PhpMixed);
+
fn get_stream(&self) -> Option<PhpMixed>;
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/string_input.rs b/crates/shirabe-external-packages/src/symfony/console/input/string_input.rs
index 34b2bfc..aad1d86 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/string_input.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/string_input.rs
@@ -1,66 +1,193 @@
-use crate::symfony::console::input::InputDefinition;
-use crate::symfony::console::input::InputInterface;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::input::argv_input::ArgvInput;
+use crate::symfony::console::input::input_definition::InputDefinition;
+use crate::symfony::console::input::input_interface::InputInterface;
use indexmap::IndexMap;
use shirabe_php_shim::PhpMixed;
+/// StringInput represents an input provided as a string.
+///
+/// Usage:
+///
+/// $input = new StringInput('foo --bar="foobar"');
#[derive(Debug)]
-pub struct StringInput;
+pub struct StringInput {
+ pub(crate) inner: ArgvInput,
+}
impl StringInput {
- pub fn new(_input: &str) -> Self {
- todo!()
+ pub const REGEX_STRING: &'static str = r#"([^\s]+?)(?:\s|(?<!\\)"|(?<!\\)'|$)"#;
+ pub const REGEX_UNQUOTED_STRING: &'static str = r#"([^\s\\]+?)"#;
+ pub const REGEX_QUOTED_STRING: &'static str =
+ r#"(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')"#;
+
+ pub fn new(input: &str) -> anyhow::Result<Self> {
+ // 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<Vec<String>> {
+ let bytes = input.as_bytes();
+ let mut tokens: Vec<String> = vec![];
+ let length = shirabe_php_shim::strlen(input);
+ let mut cursor: i64 = 0;
+ let mut token: Option<String> = 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: Vec<String> = vec![];
+ if shirabe_php_shim::preg_match_offset(r"/\s+/A", input, &mut m, 0, cursor) {
+ if token.is_some() {
+ tokens.push(token.take().unwrap());
+ }
+ cursor += shirabe_php_shim::strlen(&m[0]);
+ } else if shirabe_php_shim::preg_match_offset(
+ &format!(r#"/([^="'\s]+?)(=?)({}+)/A"#, Self::REGEX_QUOTED_STRING),
+ input,
+ &mut m,
+ 0,
+ cursor,
+ ) {
+ let inner = shirabe_php_shim::substr(&m[3], 1, Some(-1));
+ let replaced =
+ shirabe_php_shim::str_replace_arr(&["\"'", "'\"", "''", "\"\""], "", &inner);
+ token = Some(format!(
+ "{}{}{}{}",
+ token.unwrap_or_default(),
+ m[1],
+ m[2],
+ shirabe_php_shim::stripcslashes(&replaced)
+ ));
+ cursor += shirabe_php_shim::strlen(&m[0]);
+ } else if shirabe_php_shim::preg_match_offset(
+ &format!(r"/{}/A", Self::REGEX_QUOTED_STRING),
+ input,
+ &mut m,
+ 0,
+ cursor,
+ ) {
+ token = Some(format!(
+ "{}{}",
+ token.unwrap_or_default(),
+ shirabe_php_shim::stripcslashes(&shirabe_php_shim::substr(&m[0], 1, Some(-1)))
+ ));
+ cursor += shirabe_php_shim::strlen(&m[0]);
+ } else if shirabe_php_shim::preg_match_offset(
+ &format!(r"/{}/A", Self::REGEX_UNQUOTED_STRING),
+ input,
+ &mut m,
+ 0,
+ cursor,
+ ) {
+ token = Some(format!("{}{}", token.unwrap_or_default(), m[1]));
+ cursor += shirabe_php_shim::strlen(&m[0]);
+ } else {
+ // should never happen
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Unable to parse input near \"... %s ...\".",
+ &[PhpMixed::String(shirabe_php_shim::substr(
+ input,
+ cursor,
+ Some(10),
+ ))],
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+ }
+
+ if let Some(token) = token {
+ tokens.push(token);
+ }
+
+ Ok(tokens)
}
}
impl InputInterface for StringInput {
fn get_first_argument(&self) -> Option<String> {
- todo!()
+ self.inner.get_first_argument()
}
- fn has_parameter_option(&self, _values: &[&str], _only_params: bool) -> bool {
- todo!()
+
+ 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: &[&str],
- _default: PhpMixed,
- _only_params: bool,
+ values: PhpMixed,
+ default: PhpMixed,
+ only_params: bool,
) -> PhpMixed {
- todo!()
+ InputInterface::get_parameter_option(&self.inner, values, default, only_params)
}
- fn bind(&mut self, _definition: &InputDefinition) -> anyhow::Result<()> {
- todo!()
+
+ fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> {
+ InputInterface::bind(&mut self.inner, definition)
}
- fn validate(&self) -> anyhow::Result<()> {
- todo!()
+
+ fn validate(&mut self) -> anyhow::Result<()> {
+ self.inner.validate()
}
+
fn get_arguments(&self) -> IndexMap<String, PhpMixed> {
- todo!()
+ InputInterface::get_arguments(&self.inner)
}
- fn get_argument(&self, _name: &str) -> PhpMixed {
- todo!()
+
+ fn get_argument(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ self.inner.get_argument(name)
}
- fn set_argument(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
- todo!()
+
+ fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> {
+ self.inner.set_argument(name, value)
}
- fn has_argument(&self, _name: &str) -> bool {
- todo!()
+
+ fn has_argument(&self, name: &str) -> bool {
+ self.inner.has_argument(name)
}
+
fn get_options(&self) -> IndexMap<String, PhpMixed> {
- todo!()
+ InputInterface::get_options(&self.inner)
}
- fn get_option(&self, _name: &str) -> PhpMixed {
- todo!()
+
+ fn get_option(&self, name: &str) -> anyhow::Result<PhpMixed> {
+ self.inner.get_option(name)
}
- fn set_option(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
- todo!()
+
+ fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> {
+ self.inner.set_option(name, value)
}
- fn has_option(&self, _name: &str) -> bool {
- todo!()
+
+ fn has_option(&self, name: &str) -> bool {
+ self.inner.has_option(name)
}
+
fn is_interactive(&self) -> bool {
- todo!()
+ self.inner.is_interactive()
}
- fn set_interactive(&mut self, _interactive: bool) {
- todo!()
+
+ fn set_interactive(&mut self, interactive: bool) {
+ self.inner.set_interactive(interactive)
}
}