aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/input/argv_input.rs
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/argv_input.rs
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/argv_input.rs')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/argv_input.rs643
1 files changed, 643 insertions, 0 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)],
+ }
+}