aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/application.rs1701
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/command.rs10
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/help_command.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs6
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/list_command.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs8
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs6
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs6
-rw-r--r--crates/shirabe-external-packages/src/symfony/contracts/mod.rs2
-rw-r--r--crates/shirabe-external-packages/src/symfony/contracts/service/mod.rs3
-rw-r--r--crates/shirabe-external-packages/src/symfony/contracts/service/reset_interface.rs4
-rw-r--r--crates/shirabe-php-shim/src/lib.rs40
-rw-r--r--crates/shirabe/src/console/application.rs1871
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs79
-rw-r--r--crates/shirabe/src/util/platform.rs19
21 files changed, 1899 insertions, 1888 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/application.rs b/crates/shirabe-external-packages/src/symfony/console/application.rs
index 8838eaf..f46bbfa 100644
--- a/crates/shirabe-external-packages/src/symfony/console/application.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/application.rs
@@ -1,1707 +1,42 @@
use crate::symfony::console::command::command::Command;
-use crate::symfony::console::command::complete_command::CompleteCommand;
-use crate::symfony::console::command::dump_completion_command::DumpCompletionCommand;
-use crate::symfony::console::command::help_command::HelpCommand;
-use crate::symfony::console::command::lazy_command::LazyCommand;
-use crate::symfony::console::command::list_command::ListCommand;
-use crate::symfony::console::command::signalable_command_interface::SignalableCommandInterface;
-use crate::symfony::console::command_loader::command_loader_interface::CommandLoaderInterface;
use crate::symfony::console::completion::completion_input::CompletionInput;
use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions;
-use crate::symfony::console::console_events::ConsoleEvents;
-use crate::symfony::console::event::console_command_event::ConsoleCommandEvent;
-use crate::symfony::console::event::console_error_event::ConsoleErrorEvent;
-use crate::symfony::console::event::console_signal_event::ConsoleSignalEvent;
-use crate::symfony::console::event::console_terminate_event::ConsoleTerminateEvent;
-use crate::symfony::console::exception::command_not_found_exception::CommandNotFoundException;
-use crate::symfony::console::exception::exception_interface::ExceptionInterface;
-use crate::symfony::console::exception::logic_exception::LogicException;
-use crate::symfony::console::exception::namespace_not_found_exception::NamespaceNotFoundException;
-use crate::symfony::console::exception::runtime_exception::RuntimeException;
-use crate::symfony::console::formatter::output_formatter::OutputFormatter;
-use crate::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper;
-use crate::symfony::console::helper::formatter_helper::{FormatBlockMessages, FormatterHelper};
-use crate::symfony::console::helper::helper::Helper;
use crate::symfony::console::helper::helper_set::HelperSet;
-use crate::symfony::console::helper::process_helper::ProcessHelper;
-use crate::symfony::console::helper::question_helper::QuestionHelper;
-use crate::symfony::console::input::argv_input::ArgvInput;
-use crate::symfony::console::input::array_input::ArrayInput;
-use crate::symfony::console::input::input_argument::InputArgument;
-use crate::symfony::console::input::input_aware_interface::InputAwareInterface;
use crate::symfony::console::input::input_definition::InputDefinition;
-use crate::symfony::console::input::input_interface::InputInterface;
-use crate::symfony::console::input::input_option::InputOption;
-use crate::symfony::console::output::console_output::ConsoleOutput;
-use crate::symfony::console::output::console_output_interface::ConsoleOutputInterface;
-use crate::symfony::console::output::output_interface::{self, OutputInterface};
-use crate::symfony::console::signal_registry::signal_registry::SignalRegistry;
-use crate::symfony::console::style::style_interface::StyleInterface;
-use crate::symfony::console::style::symfony_style::SymfonyStyle;
-use crate::symfony::console::terminal::Terminal;
-use crate::symfony::contracts::event_dispatcher::event_dispatcher_interface::EventDispatcherInterface;
-use crate::symfony::contracts::service::reset_interface::ResetInterface;
use indexmap::IndexMap;
-use shirabe_php_shim::PhpMixed;
use std::cell::RefCell;
use std::rc::Rc;
-/// An Application is the container for a collection of commands.
-///
-/// It is the main entry point of a Console application.
-///
-/// This class is optimized for a standard CLI environment.
-#[derive(Debug)]
-pub struct Application {
- commands: IndexMap<String, Rc<RefCell<dyn Command>>>,
- want_helps: bool,
- running_command: Option<Rc<RefCell<dyn Command>>>,
- name: String,
- version: String,
- command_loader: Option<Box<dyn CommandLoaderInterface>>,
- catch_exceptions: bool,
- auto_exit: bool,
- definition: Option<Rc<RefCell<InputDefinition>>>,
- helper_set: Option<Rc<RefCell<HelperSet>>>,
- dispatcher: Option<Rc<RefCell<dyn EventDispatcherInterface>>>,
- terminal: Terminal,
- default_command: String,
- single_command: bool,
- initialized: bool,
- signal_registry: Option<SignalRegistry>,
- signals_to_dispatch_event: Vec<i64>,
-}
+/// `Symfony\Component\Console\Application` is a concrete class in PHP, but it is ported here as a
+/// trait rather than a struct.
+/// Refer to shirabe::console::Application for the reason.
+pub trait Application: std::fmt::Debug {
+ fn get_name(&self) -> String;
-impl Application {
- pub fn __construct(name: &str, version: &str) -> Self {
- let mut this = Application {
- commands: IndexMap::new(),
- want_helps: false,
- running_command: None,
- name: name.to_string(),
- version: version.to_string(),
- command_loader: None,
- catch_exceptions: true,
- auto_exit: true,
- definition: None,
- helper_set: None,
- dispatcher: None,
- terminal: Terminal::new(),
- default_command: "list".to_string(),
- single_command: false,
- initialized: false,
- signal_registry: None,
- signals_to_dispatch_event: Vec::new(),
- };
- if shirabe_php_shim::defined("SIGINT") && SignalRegistry::is_supported() {
- this.signal_registry = Some(SignalRegistry::new());
- this.signals_to_dispatch_event = vec![
- shirabe_php_shim::SIGINT,
- shirabe_php_shim::SIGTERM,
- shirabe_php_shim::SIGUSR1,
- shirabe_php_shim::SIGUSR2,
- ];
- }
- this
- }
+ fn get_version(&self) -> String;
- /// @final
- pub fn set_dispatcher(&mut self, dispatcher: Rc<RefCell<dyn EventDispatcherInterface>>) {
- // TODO(plugin): the event dispatcher drives ConsoleEvents listeners (plugins).
- self.dispatcher = Some(dispatcher);
- }
+ fn get_help(&self) -> String;
- pub fn set_command_loader(&mut self, command_loader: Box<dyn CommandLoaderInterface>) {
- self.command_loader = Some(command_loader);
- }
+ fn is_single_command(&self) -> bool;
- pub fn get_signal_registry(&self) -> anyhow::Result<&SignalRegistry> {
- match &self.signal_registry {
- None => Err(RuntimeException(shirabe_php_shim::RuntimeException {
- message: "Signals are not supported. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(),
- code: 0,
- })
- .into()),
- Some(signal_registry) => Ok(signal_registry),
- }
- }
+ fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String;
- pub fn set_signals_to_dispatch_event(&mut self, signals_to_dispatch_event: Vec<i64>) {
- self.signals_to_dispatch_event = signals_to_dispatch_event;
- }
+ fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String>;
- /// Runs the current application.
- ///
- /// Returns 0 if everything went fine, or an error code.
- ///
- /// Throws \Exception when running fails. Bypass this when set_catch_exceptions().
- pub fn run(
+ fn all(
&mut self,
- input: Option<Rc<RefCell<dyn InputInterface>>>,
- output: Option<Rc<RefCell<dyn OutputInterface>>>,
- ) -> anyhow::Result<i64> {
- if shirabe_php_shim::function_exists("putenv") {
- shirabe_php_shim::putenv(&format!("LINES={}", self.terminal.get_height()));
- shirabe_php_shim::putenv(&format!("COLUMNS={}", self.terminal.get_width()));
- }
-
- let input: Rc<RefCell<dyn InputInterface>> = match input {
- None => Rc::new(RefCell::new(ArgvInput::new(None, None)?)),
- Some(input) => input,
- };
-
- let output: Rc<RefCell<dyn OutputInterface>> = match output {
- None => Rc::new(RefCell::new(ConsoleOutput::new(None, None, None)?)),
- Some(output) => output,
- };
-
- // TODO: PHP installs a temporary `set_exception_handler($renderException)` and cooperates
- // with Symfony's ErrorHandler to keep/restore it. PHP's process-global exception handler
- // stack has no Rust equivalent; the rendering itself is invoked directly in the catch
- // branch below. Review needed for the handler save/restore dance.
- let render_exception =
- |this: &Application, e: &anyhow::Error, output: &Rc<RefCell<dyn OutputInterface>>| {
- // if ($output instanceof ConsoleOutputInterface) render to its error output
- // TODO(review): downcasting a `dyn OutputInterface` to `ConsoleOutputInterface`
- // is not directly expressible; the ConsoleOutputInterface branch needs design.
- this.render_throwable(e, output.clone());
- };
-
- let result = (|| -> anyhow::Result<i64> {
- self.configure_io(&input, &output)?;
-
- let exit_code = self.do_run(input.clone(), output.clone())?;
-
- Ok(exit_code)
- })();
-
- let mut exit_code = match result {
- Ok(exit_code) => exit_code,
- Err(e) => {
- if !self.catch_exceptions {
- return Err(e);
- }
-
- render_exception(self, &e, &output);
-
- // $exitCode = $e->getCode();
- // is_numeric($exitCode) ? max(1, (int) $exitCode) : 1
- // TODO(review): anyhow::Error has no PHP-style getCode(); the exit code derived
- // from the exception's `code` field needs the downcast strategy decided.
- let exit_code = shirabe_php_shim::php_exception_get_code(&e);
- if shirabe_php_shim::is_numeric_string(&exit_code.to_string()) {
- let exit_code = exit_code;
- if exit_code <= 0 { 1 } else { exit_code }
- } else {
- 1
- }
- }
- };
-
- // finally: handler restore. See TODO above; no-op here.
-
- if self.auto_exit {
- if exit_code > 255 {
- exit_code = 255;
- }
-
- shirabe_php_shim::exit(exit_code);
- }
-
- Ok(exit_code)
- }
-
- /// Runs the current application.
- ///
- /// Returns 0 if everything went fine, or an error code.
- pub fn do_run(
- &mut self,
- input: Rc<RefCell<dyn InputInterface>>,
- output: Rc<RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i64> {
- if input.borrow().has_parameter_option(
- PhpMixed::from(vec![
- PhpMixed::from("--version".to_string()),
- PhpMixed::from("-V".to_string()),
- ]),
- true,
- ) {
- output
- .borrow()
- .writeln(&[self.get_long_version()], output_interface::OUTPUT_NORMAL);
-
- return Ok(0);
- }
-
- // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
- match input.borrow_mut().bind(&self.get_definition().borrow()) {
- Ok(()) => {}
- Err(e) => {
- // Errors must be ignored, full binding/validation happens later when the command is known.
- if !is_exception_interface(&e) {
- return Err(e);
- }
- }
- }
-
- let mut input = input;
- let mut name = self.get_command_name(&*input.borrow());
- if input.borrow().has_parameter_option(
- PhpMixed::from(vec![
- PhpMixed::from("--help".to_string()),
- PhpMixed::from("-h".to_string()),
- ]),
- true,
- ) {
- if name.is_none() {
- name = Some("help".to_string());
- input = Rc::new(RefCell::new(ArrayInput::new(
- vec![(
- PhpMixed::from("command_name".to_string()),
- PhpMixed::from(self.default_command.clone()),
- )],
- None,
- )?));
- } else {
- self.want_helps = true;
- }
- }
-
- let name = match name {
- Some(name) => name,
- None => {
- let name = self.default_command.clone();
- let definition = self.get_definition();
- let command_description = definition
- .borrow()
- .get_argument(&PhpMixed::from("command".to_string()))?
- .get_description()
- .to_string();
- let _new_command_argument = InputArgument::new(
- "command".to_string(),
- Some(InputArgument::OPTIONAL),
- command_description,
- PhpMixed::from(name.clone()),
- )?;
- // $definition->setArguments(array_merge($definition->getArguments(),
- // ['command' => new InputArgument('command', InputArgument::OPTIONAL, ...)]))
- // TODO(review): get_arguments() yields Rc<InputArgument> (shared, non-Clone) while
- // set_arguments() consumes owned InputArgument values. Re-building the merged
- // argument list requires an InputArgument clone/ownership strategy not yet present.
- definition.borrow_mut().set_arguments(todo!(
- "merge existing arguments with the new 'command' argument"
- ))?;
-
- name
- }
- };
-
- let command: Rc<RefCell<dyn Command>>;
- let find_result = (|| -> anyhow::Result<Rc<RefCell<dyn Command>>> {
- self.running_command = None;
- // the command name MUST be the first element of the input
- self.find(&name)
- })();
-
- match find_result {
- Ok(c) => {
- command = c;
- }
- Err(e) => {
- // if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException)
- // || 1 !== count($alternatives = $e->getAlternatives()) || !$input->isInteractive())
- let alternatives: Option<Vec<String>> = downcast_command_not_found(&e)
- .filter(|_| !is_namespace_not_found(&e))
- .map(|cnf| cnf.get_alternatives().clone());
-
- let single_alternative = match &alternatives {
- Some(alts) if alts.len() == 1 => Some(alts[0].clone()),
- _ => None,
- };
-
- if single_alternative.is_none() || !input.borrow().is_interactive() {
- let mut e = e;
- if self.dispatcher.is_some() {
- // TODO(plugin): dispatch ConsoleErrorEvent so listeners can handle/replace the error.
- let _event = ConsoleErrorEvent::new(
- todo!("wrap input as Box<dyn InputInterface> for the event"),
- todo!("wrap output as Box<dyn OutputInterface> for the event"),
- todo!("wrap anyhow::Error as Box<dyn Error> for the event"),
- None,
- );
- let event: ConsoleErrorEvent = _event;
- self.dispatcher
- .as_ref()
- .unwrap()
- .borrow_mut()
- .dispatch(todo!("event object"), ConsoleEvents::ERROR);
-
- if event.get_exit_code() == 0 {
- return Ok(0);
- }
-
- e = todo!("event.get_error() converted back to anyhow::Error");
- }
-
- return Err(e);
- }
-
- let alternative = single_alternative.unwrap();
-
- let mut style = SymfonyStyle::new(input.clone(), output.clone());
- output
- .borrow()
- .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL);
- let formatted_block = FormatterHelper::default().format_block(
- FormatBlockMessages::String(format!(
- "Command \"{}\" is not defined.",
- PhpMixed::from(name.clone()),
- )),
- "error",
- true,
- );
- output
- .borrow()
- .writeln(&[formatted_block], output_interface::OUTPUT_NORMAL);
- if !style.confirm(
- &format!(
- "Do you want to run \"{}\" instead? ",
- PhpMixed::from(alternative.clone()),
- ),
- false,
- ) {
- if self.dispatcher.is_some() {
- // TODO(plugin): dispatch ConsoleErrorEvent for the declined-alternative case.
- let event = ConsoleErrorEvent::new(
- todo!("wrap input as Box<dyn InputInterface>"),
- todo!("wrap output as Box<dyn OutputInterface>"),
- todo!("wrap error as Box<dyn Error>"),
- None,
- );
- self.dispatcher
- .as_ref()
- .unwrap()
- .borrow_mut()
- .dispatch(todo!("event object"), ConsoleEvents::ERROR);
-
- return Ok(event.get_exit_code());
- }
-
- return Ok(1);
- }
-
- command = self.find(&alternative)?;
- }
- }
-
- // if ($command instanceof LazyCommand) $command = $command->getCommand();
- // TODO(review): LazyCommand is a distinct type from Command here; PHP unwraps the real
- // command. The `commands` map stores Rc<RefCell<dyn Command>>, so the LazyCommand-unwrap path
- // needs a design decision about how lazy commands are represented.
- let _ = std::marker::PhantomData::<LazyCommand>;
-
- self.running_command = Some(command.clone());
- let exit_code = self.do_run_command(command.clone(), input.clone(), output.clone())?;
- self.running_command = None;
-
- Ok(exit_code)
- }
-
- pub fn reset(&mut self) {}
-
- pub fn set_helper_set(&mut self, helper_set: Rc<RefCell<HelperSet>>) {
- self.helper_set = Some(helper_set);
- }
-
- /// Get the helper set associated with the command.
- pub fn get_helper_set(&mut self) -> Rc<RefCell<HelperSet>> {
- if self.helper_set.is_none() {
- self.helper_set = Some(self.get_default_helper_set());
- }
-
- self.helper_set.as_ref().unwrap().clone()
- }
-
- pub fn set_definition(&mut self, definition: Rc<RefCell<InputDefinition>>) {
- self.definition = Some(definition);
- }
-
- /// Gets the InputDefinition related to this Application.
- pub fn get_definition(&mut self) -> Rc<RefCell<InputDefinition>> {
- if self.definition.is_none() {
- self.definition = Some(Rc::new(RefCell::new(self.get_default_input_definition())));
- }
+ namespace: Option<&str>,
+ ) -> anyhow::Result<IndexMap<String, Rc<RefCell<dyn Command>>>>;
- if self.single_command {
- let input_definition = self.definition.as_ref().unwrap().clone();
- input_definition
- .borrow_mut()
- .set_arguments(Vec::new())
- .unwrap();
+ fn find(&mut self, name: &str) -> anyhow::Result<Rc<RefCell<dyn Command>>>;
- return input_definition;
- }
+ fn get_definition(&mut self) -> Rc<RefCell<InputDefinition>>;
- self.definition.as_ref().unwrap().clone()
- }
+ fn get_helper_set(&mut self) -> Rc<RefCell<HelperSet>>;
- /// Adds suggestions to `suggestions` for the current completion input (e.g. option or argument).
- pub fn complete(
+ fn complete(
&mut self,
input: &CompletionInput,
suggestions: &mut CompletionSuggestions,
- ) -> anyhow::Result<()> {
- if CompletionInput::TYPE_ARGUMENT_VALUE == input.get_completion_type()
- && input.get_completion_name().as_deref() == Some("command")
- {
- let mut command_names: Vec<PhpMixed> = Vec::new();
- for (name, command) in self.all(None)? {
- // skip hidden commands and aliased commands as they already get added below
- if command.borrow().is_hidden() || command.borrow().get_name() != Some(name.clone())
- {
- continue;
- }
- command_names.push(PhpMixed::from(
- command.borrow().get_name().unwrap_or_default(),
- ));
- for name in command.borrow().get_aliases() {
- command_names.push(PhpMixed::from(name));
- }
- }
- // array_filter($commandNames)
- let filtered: Vec<crate::symfony::console::completion::completion_suggestions::StringOrSuggestion> =
- command_names
- .into_iter()
- .filter(|n| shirabe_php_shim::php_truthy(n))
- .map(|n| {
- crate::symfony::console::completion::completion_suggestions::StringOrSuggestion::String(
- shirabe_php_shim::php_to_string(&n),
- )
- })
- .collect();
- suggestions.suggest_values(filtered);
-
- return Ok(());
- }
-
- if CompletionInput::TYPE_OPTION_NAME == input.get_completion_type() {
- // $suggestions->suggestOptions($this->getDefinition()->getOptions());
- // TODO(review): get_options() yields Rc<InputOption> (shared, non-Clone) while
- // suggest_options() consumes owned InputOption values; an ownership/clone strategy
- // for InputOption is needed.
- suggestions.suggest_options(todo!("owned options from get_definition().get_options()"));
-
- return Ok(());
- }
-
- Ok(())
- }
-
- /// Gets the help message.
- pub fn get_help(&self) -> String {
- self.get_long_version()
- }
-
- /// Gets whether to catch exceptions or not during commands execution.
- pub fn are_exceptions_caught(&self) -> bool {
- self.catch_exceptions
- }
-
- /// Sets whether to catch exceptions or not during commands execution.
- pub fn set_catch_exceptions(&mut self, boolean: bool) {
- self.catch_exceptions = boolean;
- }
-
- /// Gets whether to automatically exit after a command execution or not.
- pub fn is_auto_exit_enabled(&self) -> bool {
- self.auto_exit
- }
-
- /// Sets whether to automatically exit after a command execution or not.
- pub fn set_auto_exit(&mut self, boolean: bool) {
- self.auto_exit = boolean;
- }
-
- /// Gets the name of the application.
- pub fn get_name(&self) -> String {
- self.name.clone()
- }
-
- /// Sets the application name.
- pub fn set_name(&mut self, name: &str) {
- self.name = name.to_string();
- }
-
- /// Gets the application version.
- pub fn get_version(&self) -> String {
- self.version.clone()
- }
-
- /// Sets the application version.
- pub fn set_version(&mut self, version: &str) {
- self.version = version.to_string();
- }
-
- /// Returns the long version of the application.
- pub fn get_long_version(&self) -> String {
- if "UNKNOWN" != self.get_name() {
- if "UNKNOWN" != self.get_version() {
- return format!("{} <info>{}</info>", self.get_name(), self.get_version());
- }
-
- return self.get_name();
- }
-
- "Console Tool".to_string()
- }
-
- /// Adds an array of command objects.
- ///
- /// If a Command is not enabled it will not be added.
- pub fn add_commands(&mut self, commands: Vec<Rc<RefCell<dyn Command>>>) -> anyhow::Result<()> {
- for command in commands {
- self.add(command)?;
- }
- Ok(())
- }
-
- /// Adds a command object.
- ///
- /// If a command with the same name already exists, it will be overridden.
- /// If the command is not enabled it will not be added.
- pub fn add(
- &mut self,
- command: Rc<RefCell<dyn Command>>,
- ) -> anyhow::Result<Option<Rc<RefCell<dyn Command>>>> {
- self.init()?;
-
- // TODO(review): $command->setApplication($this) needs an Rc<RefCell<Application>> to the
- // current instance. Application is held by value here; the self-reference required to set
- // the command's back-pointer needs the shared-ownership design (Phase C).
- command
- .borrow_mut()
- .set_application(todo!("Rc<RefCell<Application>> of self"));
-
- if !command.borrow().is_enabled() {
- command.borrow_mut().set_application(None);
-
- return Ok(None);
- }
-
- // if (!$command instanceof LazyCommand) { $command->getDefinition(); }
- // TODO(review): LazyCommand vs Command type distinction; eager definition probe omitted
- // pending lazy-command representation decision.
- command.borrow().get_definition();
-
- if command.borrow().get_name().is_none() {
- return Err(LogicException(shirabe_php_shim::LogicException {
- message: format!(
- "The command defined in \"{}\" cannot have an empty name.",
- PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command,)),
- ),
- code: 0,
- })
- .into());
- }
-
- let name = command.borrow().get_name().unwrap();
- self.commands.insert(name, command.clone());
-
- for alias in command.borrow().get_aliases() {
- self.commands.insert(alias, command.clone());
- }
-
- Ok(Some(command))
- }
-
- /// Returns a registered command by name or alias.
- ///
- /// Throws CommandNotFoundException when given command name does not exist.
- pub fn get(&mut self, name: &str) -> anyhow::Result<Rc<RefCell<dyn Command>>> {
- self.init()?;
-
- if !self.has(name) {
- return Err(CommandNotFoundException::new(
- format!(
- "The command \"{}\" does not exist.",
- PhpMixed::from(name.to_string()),
- ),
- Vec::new(),
- 0,
- )
- .into());
- }
-
- // When the command has a different name than the one used at the command loader level
- if !self.commands.contains_key(name) {
- return Err(CommandNotFoundException::new(
- format!(
- "The \"{}\" command cannot be found because it is registered under multiple names. Make sure you don't set a different name via constructor or \"setName()\".",
- PhpMixed::from(name.to_string()),
- ),
- Vec::new(),
- 0,
- )
- .into());
- }
-
- let command = self.commands[name].clone();
-
- if self.want_helps {
- self.want_helps = false;
-
- let help_command = self.get("help")?;
- // $helpCommand->setCommand($command);
- // TODO(review): setCommand() is defined on HelpCommand, not on the concrete `Command`
- // struct; calling it through the Rc<RefCell<dyn Command>> needs the Command-subclass
- // representation decision (downcast to HelpCommand).
- let _ = &command;
- todo!("help_command.set_command(command)");
-
- #[allow(unreachable_code)]
- return Ok(help_command);
- }
-
- Ok(command)
- }
-
- /// Returns true if the command exists, false otherwise.
- pub fn has(&mut self, name: &str) -> bool {
- self.init().unwrap();
-
- if self.commands.contains_key(name) {
- return true;
- }
-
- if let Some(command_loader) = &self.command_loader {
- if command_loader.has(name) {
- let command = command_loader.get(name);
- // $this->add($this->commandLoader->get($name))
- // TODO(review): command_loader.get() returns Box<dyn Command> while add() expects
- // Rc<RefCell<dyn Command>>; the loader return type needs reconciliation.
- let _ = command;
- return self
- .add(todo!(
- "Rc<RefCell<dyn Command>> from command_loader.get(name)"
- ))
- .map(|c| c.is_some())
- .unwrap_or(false);
- }
- }
-
- false
- }
-
- /// Returns an array of all unique namespaces used by currently registered commands.
- ///
- /// It does not return the global namespace which always exists.
- pub fn get_namespaces(&mut self) -> anyhow::Result<Vec<String>> {
- let mut namespaces: Vec<Vec<String>> = Vec::new();
- for command in self.all(None)?.values() {
- if command.borrow().is_hidden() {
- continue;
- }
-
- namespaces.push(
- self.extract_all_namespaces(&command.borrow().get_name().unwrap_or_default()),
- );
-
- for alias in command.borrow().get_aliases() {
- namespaces.push(self.extract_all_namespaces(&alias));
- }
- }
-
- // array_values(array_unique(array_filter(array_merge([], ...$namespaces))))
- let mut merged: Vec<String> = Vec::new();
- for ns in namespaces {
- merged.extend(ns);
- }
- let merged: Vec<String> = merged.into_iter().filter(|s| !s.is_empty()).collect();
- let mut seen = std::collections::HashSet::new();
- let unique: Vec<String> = merged
- .into_iter()
- .filter(|s| seen.insert(s.clone()))
- .collect();
-
- Ok(unique)
- }
-
- /// Finds a registered namespace by a name or an abbreviation.
- ///
- /// Throws NamespaceNotFoundException when namespace is incorrect or ambiguous.
- pub fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String> {
- let all_namespaces = self.get_namespaces()?;
- // implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'
- let parts: Vec<String> = shirabe_php_shim::explode(":", namespace)
- .into_iter()
- .map(|p| shirabe_php_shim::preg_quote(&p, None))
- .collect();
- let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*");
- let namespaces = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_namespaces);
-
- if namespaces.is_empty() {
- let mut message = format!(
- "There are no commands defined in the \"{}\" namespace.",
- PhpMixed::from(namespace.to_string()),
- );
-
- let alternatives = self.find_alternatives(namespace, &all_namespaces);
- if !alternatives.is_empty() {
- if alternatives.len() == 1 {
- message.push_str("\n\nDid you mean this?\n ");
- } else {
- message.push_str("\n\nDid you mean one of these?\n ");
- }
-
- message.push_str(&shirabe_php_shim::implode("\n ", &alternatives));
- }
-
- return Err(NamespaceNotFoundException(CommandNotFoundException::new(
- message,
- alternatives,
- 0,
- ))
- .into());
- }
-
- let exact = namespaces.iter().any(|n| n == namespace);
- if namespaces.len() > 1 && !exact {
- return Err(NamespaceNotFoundException(CommandNotFoundException::new(
- format!(
- "The namespace \"{}\" is ambiguous.\nDid you mean one of these?\n{}.",
- PhpMixed::from(namespace.to_string()),
- PhpMixed::from(self.get_abbreviation_suggestions(&namespaces)),
- ),
- namespaces.clone(),
- 0,
- ))
- .into());
- }
-
- // $exact ? $namespace : reset($namespaces)
- if exact {
- Ok(namespace.to_string())
- } else {
- Ok(namespaces[0].clone())
- }
- }
-
- /// Finds a command by name or alias.
- ///
- /// Contrary to get, this command tries to find the best match if you give it an
- /// abbreviation of a name or alias.
- ///
- /// Throws CommandNotFoundException when command name is incorrect or ambiguous.
- pub fn find(&mut self, name: &str) -> anyhow::Result<Rc<RefCell<dyn Command>>> {
- self.init()?;
-
- let mut aliases: IndexMap<String, String> = IndexMap::new();
-
- let commands_snapshot: Vec<Rc<RefCell<dyn Command>>> =
- self.commands.values().cloned().collect();
- for command in &commands_snapshot {
- for alias in command.borrow().get_aliases() {
- if !self.has(&alias) {
- self.commands.insert(alias, command.clone());
- }
- }
- }
-
- if self.has(name) {
- return self.get(name);
- }
-
- // $allCommands = commandLoader ? array_merge(loader->getNames(), array_keys(commands)) : array_keys(commands)
- let all_commands: Vec<String> = match &self.command_loader {
- Some(command_loader) => {
- let mut all = command_loader.get_names();
- all.extend(self.commands.keys().cloned());
- all
- }
- None => self.commands.keys().cloned().collect(),
- };
-
- let parts: Vec<String> = shirabe_php_shim::explode(":", name)
- .into_iter()
- .map(|p| shirabe_php_shim::preg_quote(&p, None))
- .collect();
- let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*");
- let mut commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_commands);
-
- if commands.is_empty() {
- commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}i", expr), &all_commands);
- }
-
- // if no commands matched or we just matched namespaces
- if commands.is_empty()
- || shirabe_php_shim::preg_grep(&format!("{{^{}$}}i", expr), &commands).len() < 1
- {
- if let Some(pos) = shirabe_php_shim::strrpos(name, ":") {
- // check if a namespace exists and contains commands
- self.find_namespace(&name[..pos as usize])?;
- }
-
- let mut message = format!(
- "Command \"{}\" is not defined.",
- PhpMixed::from(name.to_string()),
- );
-
- let mut alternatives = self.find_alternatives(name, &all_commands);
- if !alternatives.is_empty() {
- // remove hidden commands
- let mut filtered: Vec<String> = Vec::new();
- for alt in alternatives {
- if !self.get(&alt)?.borrow().is_hidden() {
- filtered.push(alt);
- }
- }
- alternatives = filtered;
-
- if alternatives.len() == 1 {
- message.push_str("\n\nDid you mean this?\n ");
- } else {
- message.push_str("\n\nDid you mean one of these?\n ");
- }
- message.push_str(&shirabe_php_shim::implode("\n ", &alternatives));
- }
-
- return Err(CommandNotFoundException::new(message, alternatives, 0).into());
- }
-
- // filter out aliases for commands which are already on the list
- if commands.len() > 1 {
- // $commandList = commandLoader ? array_merge(array_flip(loader->getNames()), commands) : commands
- // TODO(review): $commandList mixes flipped loader names (string => int) with Command
- // instances; this heterogeneous PHP array needs a typed representation. The alias
- // de-duplication and the loader->get() lazy materialization are left to design.
- let mut command_list: IndexMap<String, Rc<RefCell<dyn Command>>> =
- self.commands.clone();
-
- let commands_clone = commands.clone();
- let mut new_commands: Vec<String> = Vec::new();
- let mut seen = std::collections::HashSet::new();
- for name_or_alias in commands {
- if !command_list.contains_key(&name_or_alias) {
- let loaded = self.command_loader.as_ref().unwrap().get(&name_or_alias);
- let _ = loaded;
- command_list.insert(
- name_or_alias.clone(),
- todo!("Rc<RefCell<dyn Command>> from command_loader.get(name_or_alias)"),
- );
- }
-
- let command_name = command_list[&name_or_alias]
- .borrow()
- .get_name()
- .unwrap_or_default();
-
- aliases.insert(name_or_alias.clone(), command_name.clone());
-
- let keep = command_name == name_or_alias || !commands_clone.contains(&command_name);
- if keep && seen.insert(name_or_alias.clone()) {
- new_commands.push(name_or_alias);
- }
- }
- commands = new_commands;
-
- if commands.len() > 1 {
- let usable_width = self.terminal.get_width() - 10;
- let abbrevs: Vec<String> = commands.clone();
- let mut max_len: i64 = 0;
- for abbrev in &abbrevs {
- max_len = std::cmp::max(Helper::width(abbrev), max_len);
- }
- let mut formatted_abbrevs: Vec<PhpMixed> = Vec::new();
- for cmd in commands.clone() {
- if command_list[&cmd].borrow().is_hidden() {
- // unset($commands[array_search($cmd, $commands)])
- if let Some(idx) = commands.iter().position(|c| *c == cmd) {
- commands.remove(idx);
- }
- formatted_abbrevs.push(PhpMixed::Bool(false));
- continue;
- }
-
- let abbrev = format!(
- "{} {}",
- shirabe_php_shim::str_pad(
- &cmd,
- max_len as usize,
- " ",
- shirabe_php_shim::STR_PAD_LEFT
- ),
- command_list[&cmd].borrow().get_description()
- );
-
- if Helper::width(&abbrev) > usable_width {
- formatted_abbrevs.push(PhpMixed::from(format!(
- "{}...",
- Helper::substr(&abbrev, 0, Some(usable_width - 3))
- )));
- } else {
- formatted_abbrevs.push(PhpMixed::from(abbrev));
- }
- }
-
- if commands.len() > 1 {
- let filtered: Vec<String> = formatted_abbrevs
- .iter()
- .filter(|a| shirabe_php_shim::php_truthy(a))
- .map(|a| shirabe_php_shim::php_to_string(a))
- .collect();
- let suggestions = self.get_abbreviation_suggestions(&filtered);
-
- return Err(CommandNotFoundException::new(
- format!(
- "Command \"{}\" is ambiguous.\nDid you mean one of these?\n{}.",
- PhpMixed::from(name.to_string()),
- PhpMixed::from(suggestions),
- ),
- commands.clone(),
- 0,
- )
- .into());
- }
- }
- }
-
- // $command = $this->get(reset($commands));
- let command = self.get(&commands[0])?;
-
- if command.borrow().is_hidden() {
- return Err(CommandNotFoundException::new(
- format!(
- "The command \"{}\" does not exist.",
- PhpMixed::from(name.to_string()),
- ),
- Vec::new(),
- 0,
- )
- .into());
- }
-
- Ok(command)
- }
-
- /// Gets the commands (registered in the given namespace if provided).
- ///
- /// The array keys are the full names and the values the command instances.
- pub fn all(
- &mut self,
- namespace: Option<&str>,
- ) -> anyhow::Result<IndexMap<String, Rc<RefCell<dyn Command>>>> {
- self.init()?;
-
- if namespace.is_none() {
- if self.command_loader.is_none() {
- return Ok(self.commands.clone());
- }
-
- let mut commands = self.commands.clone();
- let names = self.command_loader.as_ref().unwrap().get_names();
- for name in names {
- if !commands.contains_key(&name) && self.has(&name) {
- commands.insert(name.clone(), self.get(&name)?);
- }
- }
-
- return Ok(commands);
- }
-
- let namespace = namespace.unwrap();
- let mut commands: IndexMap<String, Rc<RefCell<dyn Command>>> = IndexMap::new();
- let entries: Vec<(String, Rc<RefCell<dyn Command>>)> = self
- .commands
- .iter()
- .map(|(k, v)| (k.clone(), v.clone()))
- .collect();
- for (name, command) in entries {
- if namespace
- == self.extract_namespace(
- &name,
- Some(shirabe_php_shim::substr_count(namespace, ":") + 1),
- )
- {
- commands.insert(name, command);
- }
- }
-
- if self.command_loader.is_some() {
- let names = self.command_loader.as_ref().unwrap().get_names();
- for name in names {
- if !commands.contains_key(&name)
- && namespace
- == self.extract_namespace(
- &name,
- Some(shirabe_php_shim::substr_count(namespace, ":") + 1),
- )
- && self.has(&name)
- {
- commands.insert(name.clone(), self.get(&name)?);
- }
- }
- }
-
- Ok(commands)
- }
-
- /// Returns an array of possible abbreviations given a set of names.
- pub fn get_abbreviations(names: Vec<String>) -> IndexMap<String, Vec<String>> {
- let mut abbrevs: IndexMap<String, Vec<String>> = IndexMap::new();
- for name in names {
- let mut len = shirabe_php_shim::strlen(&name);
- while len > 0 {
- let abbrev = shirabe_php_shim::substr(&name, 0, Some(len));
- abbrevs.entry(abbrev).or_default().push(name.clone());
- len -= 1;
- }
- }
-
- abbrevs
- }
-
- pub fn render_throwable(&self, e: &anyhow::Error, output: Rc<RefCell<dyn OutputInterface>>) {
- output
- .borrow()
- .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET);
-
- self.do_render_throwable(e, output.clone());
-
- if let Some(running_command) = &self.running_command {
- output.borrow().writeln(
- &[format!(
- "<info>{}</info>",
- PhpMixed::from(
- OutputFormatter::escape(&shirabe_php_shim::sprintf(
- &running_command.borrow_mut().get_synopsis(false),
- &[PhpMixed::from(self.get_name())],
- ))
- .unwrap(),
- ),
- )],
- output_interface::VERBOSITY_QUIET,
- );
- output
- .borrow()
- .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET);
- }
- }
-
- pub fn do_render_throwable(&self, e: &anyhow::Error, output: Rc<RefCell<dyn OutputInterface>>) {
- // do { ... } while ($e = $e->getPrevious());
- // TODO(review): PHP walks the exception chain via getPrevious() and reads getMessage(),
- // getCode(), getFile(), getLine(), getTrace(). anyhow::Error exposes a source() chain but
- // not file/line/trace; faithful rendering of the trace needs a Throwable-equivalent.
- let _ = output;
- let _ = e;
- todo!("render exception chain (getMessage/getCode/getFile/getLine/getTrace/getPrevious)")
- }
-
- /// Configures the input and output instances based on the user arguments and options.
- pub fn configure_io(
- &self,
- input: &Rc<RefCell<dyn InputInterface>>,
- output: &Rc<RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<()> {
- if input.borrow().has_parameter_option(
- PhpMixed::from(vec![PhpMixed::from("--ansi".to_string())]),
- true,
- ) {
- output.borrow().set_decorated(true);
- } else if input.borrow().has_parameter_option(
- PhpMixed::from(vec![PhpMixed::from("--no-ansi".to_string())]),
- true,
- ) {
- output.borrow().set_decorated(false);
- }
-
- if input.borrow().has_parameter_option(
- PhpMixed::from(vec![
- PhpMixed::from("--no-interaction".to_string()),
- PhpMixed::from("-n".to_string()),
- ]),
- true,
- ) {
- input.borrow_mut().set_interactive(false);
- }
-
- let mut shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY").unwrap_or_default();
- let shell_verbosity_int: i64 = shell_verbosity.parse().unwrap_or(0);
- let mut shell_verbosity: i64 = shell_verbosity_int;
- match shell_verbosity_int {
- -1 => {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_QUIET);
- }
- 1 => {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_VERBOSE);
- }
- 2 => {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE);
- }
- 3 => {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_DEBUG);
- }
- _ => {
- shell_verbosity = 0;
- }
- }
-
- if input.borrow().has_parameter_option(
- PhpMixed::from(vec![
- PhpMixed::from("--quiet".to_string()),
- PhpMixed::from("-q".to_string()),
- ]),
- true,
- ) {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_QUIET);
- shell_verbosity = -1;
- } else if input
- .borrow()
- .has_parameter_option(PhpMixed::from("-vvv".to_string()), true)
- || input
- .borrow()
- .has_parameter_option(PhpMixed::from("--verbose=3".to_string()), true)
- || input.borrow().get_parameter_option(
- PhpMixed::from("--verbose".to_string()),
- PhpMixed::Bool(false),
- true,
- ) == PhpMixed::from(3i64)
- {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_DEBUG);
- shell_verbosity = 3;
- } else if input
- .borrow()
- .has_parameter_option(PhpMixed::from("-vv".to_string()), true)
- || input
- .borrow()
- .has_parameter_option(PhpMixed::from("--verbose=2".to_string()), true)
- || input.borrow().get_parameter_option(
- PhpMixed::from("--verbose".to_string()),
- PhpMixed::Bool(false),
- true,
- ) == PhpMixed::from(2i64)
- {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE);
- shell_verbosity = 2;
- } else if input
- .borrow()
- .has_parameter_option(PhpMixed::from("-v".to_string()), true)
- || input
- .borrow()
- .has_parameter_option(PhpMixed::from("--verbose=1".to_string()), true)
- || input
- .borrow()
- .has_parameter_option(PhpMixed::from("--verbose".to_string()), true)
- || shirabe_php_shim::php_truthy(&input.borrow().get_parameter_option(
- PhpMixed::from("--verbose".to_string()),
- PhpMixed::Bool(false),
- true,
- ))
- {
- output
- .borrow()
- .set_verbosity(output_interface::VERBOSITY_VERBOSE);
- shell_verbosity = 1;
- }
-
- if shell_verbosity == -1 {
- input.borrow_mut().set_interactive(false);
- }
-
- if shirabe_php_shim::function_exists("putenv") {
- shirabe_php_shim::putenv(&format!("SHELL_VERBOSITY={}", shell_verbosity));
- }
- shirabe_php_shim::env_set("SHELL_VERBOSITY", shell_verbosity.to_string());
- shirabe_php_shim::server_set("SHELL_VERBOSITY", shell_verbosity.to_string());
-
- let _ = &mut shell_verbosity;
-
- Ok(())
- }
-
- /// Runs the current command.
- ///
- /// If an event dispatcher has been attached to the application, events are also
- /// dispatched during the life-cycle of the command.
- ///
- /// Returns 0 if everything went fine, or an error code.
- pub fn do_run_command(
- &mut self,
- command: Rc<RefCell<dyn Command>>,
- input: Rc<RefCell<dyn InputInterface>>,
- output: Rc<RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i64> {
- if let Some(helper_set) = command.borrow().get_helper_set() {
- for (_alias, helper) in helper_set.borrow().get_iterator() {
- // if ($helper instanceof InputAwareInterface) $helper->setInput($input);
- // TODO(review): downcasting a HelperInterface to InputAwareInterface is not
- // expressible without a typed mechanism; needs design.
- let _ = helper;
- let _ = std::marker::PhantomData::<dyn InputAwareInterface>;
- }
- }
-
- if !self.signals_to_dispatch_event.is_empty() {
- // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []
- // TODO(review): Command is not a SignalableCommandInterface here; downcast needed.
- let command_signals: Vec<i64> = Vec::new();
- let _ = std::marker::PhantomData::<dyn SignalableCommandInterface>;
-
- if !command_signals.is_empty() || self.dispatcher.is_some() {
- if self.signal_registry.is_none() {
- return Err(RuntimeException(shirabe_php_shim::RuntimeException {
- message: "Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(),
- code: 0,
- })
- .into());
- }
-
- if Terminal::has_stty_available() {
- // TODO: registers SIGINT/SIGTERM handlers that restore the stty mode via
- // shell_exec('stty ...'). pcntl signal handlers have no faithful Rust
- // equivalent in Phase A.
- let _stty_mode = shirabe_php_shim::shell_exec("stty -g");
- for _signal in [shirabe_php_shim::SIGINT, shirabe_php_shim::SIGTERM] {
- todo!("register signal handler to restore stty mode");
- }
- }
- }
-
- if self.dispatcher.is_some() {
- // TODO(plugin): for each signal, register a handler that dispatches ConsoleSignalEvent.
- for &signal in &self.signals_to_dispatch_event.clone() {
- let _event = ConsoleSignalEvent::new(
- todo!("Box<dyn Command>"),
- todo!("Box<dyn InputInterface>"),
- todo!("Box<dyn OutputInterface>"),
- signal,
- );
- todo!("register signal handler dispatching ConsoleEvents::SIGNAL");
- }
- }
-
- for _signal in command_signals {
- // $this->signalRegistry->register($signal, [$command, 'handleSignal']);
- todo!("register command->handle_signal as signal handler");
- }
- }
-
- if self.dispatcher.is_none() {
- return command.borrow_mut().run(
- &mut *borrow_input_mut(&input),
- &mut *borrow_output_mut(&output),
- );
- }
-
- // bind before the console.command event, so the listeners have access to input options/arguments
- match (|| -> anyhow::Result<()> {
- command.borrow_mut().merge_application_definition(true);
- input.borrow_mut().bind(command.borrow().get_definition())?;
- Ok(())
- })() {
- Ok(()) => {}
- Err(e) => {
- // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
- if !is_exception_interface(&e) {
- return Err(e);
- }
- }
- }
-
- // TODO(plugin): the whole dispatcher block below drives ConsoleCommandEvent /
- // ConsoleErrorEvent / ConsoleTerminateEvent. The event objects require Box<dyn ...>
- // wrappers for input/output/command and the dispatcher's dispatch() contract; their
- // construction is left to the plugin/event design.
- let _ = ConsoleCommandEvent::RETURN_CODE_DISABLED;
- let _ = std::marker::PhantomData::<(
- ConsoleCommandEvent,
- ConsoleErrorEvent,
- ConsoleTerminateEvent,
- )>;
- todo!("dispatcher-driven command run (console.command / console.error / console.terminate)")
- }
-
- /// Gets the name of the command based on input.
- pub fn get_command_name(&self, input: &dyn InputInterface) -> Option<String> {
- if self.single_command {
- Some(self.default_command.clone())
- } else {
- input.get_first_argument()
- }
- }
-
- /// Gets the default input definition.
- pub fn get_default_input_definition(&self) -> InputDefinition {
- use crate::symfony::console::input::input_definition::DefinitionItem;
- InputDefinition::new(vec![
- DefinitionItem::InputArgument(
- InputArgument::new(
- "command".to_string(),
- Some(InputArgument::REQUIRED),
- "The command to execute".to_string(),
- PhpMixed::Null,
- )
- .unwrap(),
- ),
- DefinitionItem::InputOption(
- InputOption::new(
- "--help",
- PhpMixed::from("-h".to_string()),
- Some(InputOption::VALUE_NONE),
- format!(
- "Display help for the given command. When no command is given display help for the <info>{}</info> command",
- self.default_command
- ),
- PhpMixed::Null,
- )
- .unwrap(),
- ),
- DefinitionItem::InputOption(
- InputOption::new(
- "--quiet",
- PhpMixed::from("-q".to_string()),
- Some(InputOption::VALUE_NONE),
- "Do not output any message".to_string(),
- PhpMixed::Null,
- )
- .unwrap(),
- ),
- DefinitionItem::InputOption(
- InputOption::new(
- "--verbose",
- PhpMixed::from("-v|vv|vvv".to_string()),
- Some(InputOption::VALUE_NONE),
- "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug".to_string(),
- PhpMixed::Null,
- )
- .unwrap(),
- ),
- DefinitionItem::InputOption(
- InputOption::new(
- "--version",
- PhpMixed::from("-V".to_string()),
- Some(InputOption::VALUE_NONE),
- "Display this application version".to_string(),
- PhpMixed::Null,
- )
- .unwrap(),
- ),
- DefinitionItem::InputOption(
- InputOption::new(
- "--ansi",
- PhpMixed::from("".to_string()),
- Some(InputOption::VALUE_NEGATABLE),
- "Force (or disable --no-ansi) ANSI output".to_string(),
- PhpMixed::Null,
- )
- .unwrap(),
- ),
- DefinitionItem::InputOption(
- InputOption::new(
- "--no-interaction",
- PhpMixed::from("-n".to_string()),
- Some(InputOption::VALUE_NONE),
- "Do not ask any interactive question".to_string(),
- PhpMixed::Null,
- )
- .unwrap(),
- ),
- ])
- .unwrap()
- }
-
- /// Gets the default commands that should always be available.
- pub fn get_default_commands(&self) -> Vec<Rc<RefCell<dyn Command>>> {
- // return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
- // TODO(review): HelpCommand/ListCommand/CompleteCommand/DumpCompletionCommand are ported as
- // distinct structs (not subtypes of the concrete `Command` struct), so they cannot populate
- // a Vec<Rc<RefCell<dyn Command>>>. Reconciling Command subclassing with Rust requires the
- // command-hierarchy design decision (see also `add`/`find`/LazyCommand handling).
- let _ = std::marker::PhantomData::<(
- HelpCommand,
- ListCommand,
- CompleteCommand,
- DumpCompletionCommand,
- )>;
- todo!("construct default commands once Command-subclass representation is decided")
- }
-
- /// Gets the default helper set with the helpers that should always be available.
- pub fn get_default_helper_set(&self) -> Rc<RefCell<HelperSet>> {
- use crate::symfony::console::helper::helper_interface::HelperInterface;
- let helper_set = Rc::new(RefCell::new(HelperSet::default()));
- let helpers: IndexMap<
- crate::symfony::console::helper::helper_set::HelperSetKey,
- Rc<RefCell<dyn HelperInterface>>,
- > = {
- let mut m: IndexMap<
- crate::symfony::console::helper::helper_set::HelperSetKey,
- Rc<RefCell<dyn HelperInterface>>,
- > = IndexMap::new();
- m.insert(
- crate::symfony::console::helper::helper_set::HelperSetKey::Int(0),
- Rc::new(RefCell::new(FormatterHelper::default())),
- );
- m.insert(
- crate::symfony::console::helper::helper_set::HelperSetKey::Int(1),
- Rc::new(RefCell::new(DebugFormatterHelper::default())),
- );
- m.insert(
- crate::symfony::console::helper::helper_set::HelperSetKey::Int(2),
- Rc::new(RefCell::new(ProcessHelper::default())),
- );
- m.insert(
- crate::symfony::console::helper::helper_set::HelperSetKey::Int(3),
- Rc::new(RefCell::new(QuestionHelper::default())),
- );
- m
- };
- HelperSet::new(&helper_set, helpers);
- helper_set
- }
-
- /// Returns abbreviated suggestions in string format.
- fn get_abbreviation_suggestions(&self, abbrevs: &[String]) -> String {
- format!(" {}", shirabe_php_shim::implode("\n ", abbrevs))
- }
-
- /// Returns the namespace part of the command name.
- ///
- /// This method is not part of public API and should not be used directly.
- pub fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String {
- // $parts = explode(':', $name, -1);
- let parts = shirabe_php_shim::explode_limit(":", name, -1);
-
- // implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit))
- match limit {
- None => shirabe_php_shim::implode(":", &parts),
- Some(limit) => {
- let sliced: Vec<String> = parts.into_iter().take(limit.max(0) as usize).collect();
- shirabe_php_shim::implode(":", &sliced)
- }
- }
- }
-
- /// Finds alternative of $name among $collection, if nothing is found in
- /// $collection, try in $abbrevs.
- fn find_alternatives(&self, name: &str, collection: &[String]) -> Vec<String> {
- let threshold = 1e3;
- let mut alternatives: IndexMap<String, f64> = IndexMap::new();
-
- let mut collection_parts: IndexMap<String, Vec<String>> = IndexMap::new();
- for item in collection {
- collection_parts.insert(item.clone(), shirabe_php_shim::explode(":", item));
- }
-
- for (i, subname) in shirabe_php_shim::explode(":", name).into_iter().enumerate() {
- for (collection_name, parts) in &collection_parts {
- let exists = alternatives.contains_key(collection_name);
- if parts.get(i).is_none() && exists {
- *alternatives.get_mut(collection_name).unwrap() += threshold;
- continue;
- } else if parts.get(i).is_none() {
- continue;
- }
-
- let lev = shirabe_php_shim::levenshtein(&subname, &parts[i]) as f64;
- if lev <= shirabe_php_shim::strlen(&subname) as f64 / 3.0
- || (!subname.is_empty() && parts[i].contains(&subname))
- {
- let v = if exists {
- alternatives[collection_name] + lev
- } else {
- lev
- };
- alternatives.insert(collection_name.clone(), v);
- } else if exists {
- *alternatives.get_mut(collection_name).unwrap() += threshold;
- }
- }
- }
-
- for item in collection {
- let lev = shirabe_php_shim::levenshtein(name, item) as f64;
- if lev <= shirabe_php_shim::strlen(name) as f64 / 3.0 || item.contains(name) {
- let v = if alternatives.contains_key(item) {
- alternatives[item] - lev
- } else {
- lev
- };
- alternatives.insert(item.clone(), v);
- }
- }
-
- // array_filter($alternatives, fn($lev) => $lev < 2 * $threshold)
- alternatives.retain(|_, lev| *lev < 2.0 * threshold);
- // ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE)
- let mut keys: Vec<String> = alternatives.keys().cloned().collect();
- shirabe_php_shim::sort_natural_flag_case(&mut keys);
-
- keys
- }
-
- /// Sets the default Command name.
- pub fn set_default_command(
- &mut self,
- command_name: &str,
- is_single_command: bool,
- ) -> anyhow::Result<&mut Self> {
- // $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];
- let trimmed = shirabe_php_shim::ltrim(command_name, Some("|"));
- self.default_command = shirabe_php_shim::explode("|", &trimmed)
- .into_iter()
- .next()
- .unwrap_or_default();
-
- if is_single_command {
- // Ensure the command exist
- self.find(command_name)?;
-
- self.single_command = true;
- }
-
- Ok(self)
- }
-
- /// @internal
- pub fn is_single_command(&self) -> bool {
- self.single_command
- }
-
- fn split_string_by_width(&self, string: &str, width: i64) -> Vec<String> {
- // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
- let encoding = match shirabe_php_shim::mb_detect_encoding(string, None, true) {
- None => return shirabe_php_shim::str_split(string, width),
- Some(encoding) => encoding,
- };
-
- let utf8_string = shirabe_php_shim::mb_convert_encoding(string.into(), "utf8", &encoding);
- let mut lines: Vec<String> = Vec::new();
- let mut line = String::new();
-
- let mut offset = 0i64;
- let mut m: Vec<String> = Vec::new();
- while shirabe_php_shim::preg_match_offset(r"/.{1,10000}/u", &utf8_string, &mut m, 0, offset)
- {
- offset += shirabe_php_shim::strlen(&m[0]);
-
- for char in shirabe_php_shim::preg_split_chars(r"//u", &m[0]) {
- // test if $char could be appended to current line
- if shirabe_php_shim::mb_strwidth(&format!("{}{}", line, char), Some("utf8"))
- <= width
- {
- line.push_str(&char);
- continue;
- }
- // if not, push current line to array and make new line
- lines.push(shirabe_php_shim::str_pad(
- &line,
- width as usize,
- " ",
- shirabe_php_shim::STR_PAD_LEFT,
- ));
- line = char;
- }
- }
-
- lines.push(if !lines.is_empty() {
- shirabe_php_shim::str_pad(&line, width as usize, " ", shirabe_php_shim::STR_PAD_LEFT)
- } else {
- line.clone()
- });
-
- shirabe_php_shim::mb_convert_variables(&encoding, "utf8", &mut lines);
-
- lines
- }
-
- /// Returns all namespaces of the command name.
- fn extract_all_namespaces(&self, name: &str) -> Vec<String> {
- // -1 as third argument is needed to skip the command short name when exploding
- let parts = shirabe_php_shim::explode_limit(":", name, -1);
- let mut namespaces: Vec<String> = Vec::new();
-
- for part in parts {
- if !namespaces.is_empty() {
- let last = namespaces.last().unwrap().clone();
- namespaces.push(format!("{}:{}", last, part));
- } else {
- namespaces.push(part);
- }
- }
-
- namespaces
- }
-
- fn init(&mut self) -> anyhow::Result<()> {
- if self.initialized {
- return Ok(());
- }
- self.initialized = true;
-
- for command in self.get_default_commands() {
- self.add(command)?;
- }
-
- Ok(())
- }
-}
-
-impl ResetInterface for Application {
- fn reset(&mut self) {
- Application::reset(self)
- }
-}
-
-/// Helper mirroring PHP's `$e instanceof ExceptionInterface`.
-fn is_exception_interface(e: &anyhow::Error) -> bool {
- // anyhow::Error stores concrete error types; enumerate the console exceptions
- // that implement ExceptionInterface (PHP's `$e instanceof ExceptionInterface`).
- e.downcast_ref::<CommandNotFoundException>().is_some()
- || e.downcast_ref::<NamespaceNotFoundException>().is_some()
- || e.downcast_ref::<LogicException>().is_some()
- || e.downcast_ref::<RuntimeException>().is_some()
-}
-
-/// Helper mirroring PHP's `$e instanceof CommandNotFoundException`.
-fn downcast_command_not_found(e: &anyhow::Error) -> Option<&CommandNotFoundException> {
- if let Some(cnf) = e.downcast_ref::<CommandNotFoundException>() {
- return Some(cnf);
- }
- e.downcast_ref::<NamespaceNotFoundException>().map(|n| &n.0)
-}
-
-/// Helper mirroring PHP's `$e instanceof NamespaceNotFoundException`.
-fn is_namespace_not_found(e: &anyhow::Error) -> bool {
- e.downcast_ref::<NamespaceNotFoundException>().is_some()
-}
-
-/// Borrows the shared input as a mutable `dyn InputInterface` for passing to
-/// `Command::run`, which takes `&mut dyn InputInterface`.
-fn borrow_input_mut(
- input: &Rc<RefCell<dyn InputInterface>>,
-) -> std::cell::RefMut<'_, dyn InputInterface> {
- input.borrow_mut()
-}
-
-/// Borrows the shared output as a mutable `dyn OutputInterface` for passing to
-/// `Command::run`, which takes `&mut dyn OutputInterface`.
-fn borrow_output_mut(
- output: &Rc<RefCell<dyn OutputInterface>>,
-) -> std::cell::RefMut<'_, dyn OutputInterface> {
- output.borrow_mut()
+ ) -> anyhow::Result<()>;
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/command.rs b/crates/shirabe-external-packages/src/symfony/console/command/command.rs
index 1af9c59..1640689 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/command.rs
@@ -20,7 +20,7 @@ use std::rc::Rc;
/// (defined below) and this concrete `BaseCommand` struct holding the base-class
/// state and behavior. Subclasses embed a `BaseCommand` and implement `Command`.
pub struct BaseCommand {
- application: Option<Rc<RefCell<Application>>>,
+ application: Option<Rc<RefCell<dyn Application>>>,
name: Option<String>,
process_title: Option<String>,
aliases: Vec<String>,
@@ -130,7 +130,7 @@ impl BaseCommand {
self.ignore_validation_errors = true;
}
- pub fn set_application(&mut self, application: Option<Rc<RefCell<Application>>>) {
+ pub fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) {
self.application = application.clone();
if let Some(application) = application {
self.set_helper_set(application.borrow_mut().get_helper_set());
@@ -151,7 +151,7 @@ impl BaseCommand {
}
/// Gets the application instance for this command.
- pub fn get_application(&self) -> Option<Rc<RefCell<Application>>> {
+ pub fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> {
self.application.clone()
}
@@ -709,11 +709,11 @@ pub trait Command: std::fmt::Debug + shirabe_php_shim::AsAny {
todo!()
}
- fn set_application(&mut self, _application: Option<Rc<RefCell<Application>>>) {
+ fn set_application(&mut self, _application: Option<Rc<RefCell<dyn Application>>>) {
todo!()
}
- fn get_application(&self) -> Option<Rc<RefCell<Application>>> {
+ fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> {
todo!()
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs
index 7a986ec..e8a319e 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs
@@ -396,14 +396,14 @@ impl Command for CompleteCommand {
fn set_application(
&mut self,
- application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>,
+ application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>,
) {
self.inner.set_application(application);
}
fn get_application(
&self,
- ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> {
+ ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> {
self.inner.get_application()
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
index 51287c7..1929c64 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
@@ -230,14 +230,14 @@ impl Command for DumpCompletionCommand {
fn set_application(
&mut self,
- application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>,
+ application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>,
) {
self.inner.set_application(application);
}
fn get_application(
&self,
- ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> {
+ ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> {
self.inner.get_application()
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs
index 0a26931..ec2de93 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs
@@ -162,14 +162,14 @@ impl Command for HelpCommand {
fn set_application(
&mut self,
- application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>,
+ application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>,
) {
self.inner.set_application(application);
}
fn get_application(
&self,
- ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> {
+ ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> {
self.inner.get_application()
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs
index 47f73bf..924d4bd 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs
@@ -78,7 +78,7 @@ impl LazyCommand {
self.get_command().ignore_validation_errors();
}
- pub fn set_application(&mut self, application: Option<Rc<RefCell<Application>>>) {
+ pub fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) {
// if ($this->command instanceof parent)
if let LazyCommandInner::Command(command) = &mut self.command {
command.set_application(application.clone());
@@ -288,11 +288,11 @@ impl Command for LazyCommand {
todo!()
}
- fn set_application(&mut self, application: Option<Rc<RefCell<Application>>>) {
+ fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) {
LazyCommand::set_application(self, application);
}
- fn get_application(&self) -> Option<Rc<RefCell<Application>>> {
+ fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> {
self.inner.get_application()
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs
index 7581c90..5203be1 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs
@@ -162,14 +162,14 @@ impl Command for ListCommand {
fn set_application(
&mut self,
- application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>,
+ application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>,
) {
self.inner.set_application(application);
}
fn get_application(
&self,
- ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> {
+ ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> {
self.inner.get_application()
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs
index 3454b78..ff14c22 100644
--- a/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs
@@ -9,7 +9,7 @@ use std::rc::Rc;
/// @internal
#[derive(Debug)]
pub struct ApplicationDescription {
- application: Rc<RefCell<Application>>,
+ application: Rc<RefCell<dyn Application>>,
namespace: Option<String>,
show_hidden: bool,
@@ -28,7 +28,7 @@ impl ApplicationDescription {
pub const GLOBAL_NAMESPACE: &'static str = "_global";
pub fn new(
- application: Rc<RefCell<Application>>,
+ application: Rc<RefCell<dyn Application>>,
namespace: Option<String>,
show_hidden: bool,
) -> Self {
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs
index d4aa575..d086b28 100644
--- a/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs
@@ -48,7 +48,7 @@ pub trait Descriptor: DescriptorInterface {
}
// case $object instanceof Application:
_ if todo!("$object instanceof Application") => {
- let application: std::rc::Rc<std::cell::RefCell<Application>> =
+ let application: std::rc::Rc<std::cell::RefCell<dyn Application>> =
todo!("downcast object to Application");
self.describe_application(application, options)?;
}
@@ -113,7 +113,7 @@ pub trait Descriptor: DescriptorInterface {
/// Describes an Application instance.
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()>;
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs
index b651618..a74fc4b 100644
--- a/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs
@@ -62,7 +62,7 @@ impl JsonDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
let described_namespace = match options.get("namespace") {
@@ -418,7 +418,7 @@ impl Descriptor for JsonDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
JsonDescriptor::describe_application(self, application, options)
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs
index 1fec085..90cad25 100644
--- a/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs
@@ -214,7 +214,7 @@ impl MarkdownDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
let described_namespace = match options.get("namespace") {
@@ -223,7 +223,7 @@ impl MarkdownDescriptor {
};
let mut description =
ApplicationDescription::new(application.clone(), described_namespace, false);
- let title = self.get_application_title(&application.borrow());
+ let title = self.get_application_title(&*application.borrow());
self.write(
&format!(
@@ -289,7 +289,7 @@ impl MarkdownDescriptor {
Ok(())
}
- fn get_application_title(&self, application: &Application) -> String {
+ fn get_application_title(&self, application: &dyn Application) -> String {
if "UNKNOWN" != application.get_name() {
if "UNKNOWN" != application.get_version() {
return format!(
@@ -376,7 +376,7 @@ impl Descriptor for MarkdownDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
MarkdownDescriptor::describe_application(self, application, options)
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs
index b5faaae..323b284 100644
--- a/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs
@@ -262,7 +262,7 @@ impl TextDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
let described_namespace = match options.get("namespace") {
@@ -597,7 +597,7 @@ impl Descriptor for TextDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
TextDescriptor::describe_application(self, application, options)
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs
index 209964a..2ed8f17 100644
--- a/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs
@@ -106,7 +106,7 @@ impl XmlDescriptor {
pub fn get_application_document(
&self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
namespace: Option<String>,
short: bool,
) -> DOMDocument {
@@ -209,7 +209,7 @@ impl XmlDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
let namespace = match options.get("namespace") {
@@ -420,7 +420,7 @@ impl Descriptor for XmlDescriptor {
fn describe_application(
&mut self,
- application: std::rc::Rc<std::cell::RefCell<Application>>,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
options: IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
XmlDescriptor::describe_application(self, application, options)
diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs
index 2b290db..71474bc 100644
--- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs
@@ -1,7 +1,6 @@
use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
use crate::symfony::console::formatter::output_formatter_style::OutputFormatterStyle;
use crate::symfony::console::formatter::output_formatter_style_interface::OutputFormatterStyleInterface;
-use crate::symfony::contracts::service::reset_interface::ResetInterface;
#[derive(Debug)]
pub struct OutputFormatterStyleStack {
@@ -98,11 +97,8 @@ impl OutputFormatterStyleStack {
pub fn get_empty_style(&self) -> &dyn OutputFormatterStyleInterface {
self.empty_style.as_ref()
}
-}
-impl ResetInterface for OutputFormatterStyleStack {
- /// Resets stack (ie. empty internal arrays).
- fn reset(&mut self) {
+ pub fn reset(&mut self) {
self.styles = vec![];
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/contracts/mod.rs b/crates/shirabe-external-packages/src/symfony/contracts/mod.rs
index ad1b417..36260a8 100644
--- a/crates/shirabe-external-packages/src/symfony/contracts/mod.rs
+++ b/crates/shirabe-external-packages/src/symfony/contracts/mod.rs
@@ -1,5 +1,3 @@
pub mod event_dispatcher;
-pub mod service;
pub use event_dispatcher::*;
-pub use service::*;
diff --git a/crates/shirabe-external-packages/src/symfony/contracts/service/mod.rs b/crates/shirabe-external-packages/src/symfony/contracts/service/mod.rs
deleted file mode 100644
index 5dbf535..0000000
--- a/crates/shirabe-external-packages/src/symfony/contracts/service/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod reset_interface;
-
-pub use reset_interface::*;
diff --git a/crates/shirabe-external-packages/src/symfony/contracts/service/reset_interface.rs b/crates/shirabe-external-packages/src/symfony/contracts/service/reset_interface.rs
deleted file mode 100644
index cdfe2cf..0000000
--- a/crates/shirabe-external-packages/src/symfony/contracts/service/reset_interface.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-/// Mirror of Symfony's `ResetInterface`.
-pub trait ResetInterface {
- fn reset(&mut self);
-}
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index 2142a04..59626ea 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -991,8 +991,20 @@ pub trait JsonSerializable {
fn json_serialize(&self) -> PhpMixed;
}
-pub fn in_array(_needle: PhpMixed, _haystack: &PhpMixed, _strict: bool) -> bool {
- todo!()
+pub fn in_array(needle: PhpMixed, haystack: &PhpMixed, strict: bool) -> bool {
+ let values: Vec<&PhpMixed> = match haystack {
+ PhpMixed::List(items) => items.iter().map(|item| item.as_ref()).collect(),
+ PhpMixed::Array(map) => map.values().map(|item| item.as_ref()).collect(),
+ _ => return false,
+ };
+
+ if !strict {
+ // TODO(phase-c): non-strict in_array needs PHP's loose `==` comparison semantics. Only the
+ // strict path is implemented; loose comparison is deferred rather than approximated.
+ todo!("non-strict in_array (PHP loose comparison)");
+ }
+
+ values.iter().any(|value| **value == needle)
}
// TODO(phase-c): takes &Path and returns Option<PathBuf>
@@ -1963,8 +1975,8 @@ pub fn mb_strlen(_s: &str, _encoding: &str) -> i64 {
todo!()
}
-pub fn stream_isatty(_stream: PhpMixed) -> bool {
- todo!()
+pub fn stream_isatty(stream: PhpResource) -> bool {
+ stream_isatty_resource(&stream)
}
pub fn posix_getuid() -> i64 {
@@ -1979,11 +1991,11 @@ pub fn posix_getpwuid(_uid: i64) -> PhpMixed {
todo!()
}
-pub fn posix_isatty(_stream: PhpMixed) -> bool {
+pub fn posix_isatty(_stream: PhpResource) -> bool {
todo!()
}
-pub fn fstat(_stream: PhpMixed) -> PhpMixed {
+pub fn fstat(_stream: PhpResource) -> PhpMixed {
todo!()
}
@@ -2010,6 +2022,7 @@ pub fn putenv(setting: &str) -> bool {
/// PHP superglobal $_SERVER access. In the CLI SAPI $_SERVER is populated from
/// the environment, which is the only source modeled here.
pub fn server_get(name: &str) -> Option<String> {
+ // TODO: is var_os() better?
std::env::var(name).ok()
}
@@ -2023,9 +2036,10 @@ pub fn server_contains_key(name: &str) -> bool {
std::env::var_os(name).is_some()
}
-/// PHP superglobal $_ENV access
-pub fn env_get(_name: &str) -> Option<String> {
- todo!()
+/// PHP superglobal $_ENV access.
+pub fn env_get(name: &str) -> Option<String> {
+ // TODO: is var_os() better?
+ std::env::var(name).ok()
}
// TODO(php-runtime): modify the real PHP's $_ENV.
@@ -2034,8 +2048,8 @@ pub fn env_set(_name: &str, _value: String) {}
// TODO(php-runtime): modify the real PHP's $_ENV.
pub fn env_unset(_name: &str) {}
-pub fn env_contains_key(_name: &str) -> bool {
- todo!()
+pub fn env_contains_key(name: &str) -> bool {
+ std::env::var_os(name).is_some()
}
pub fn trim(_s: &str, _chars: Option<&str>) -> String {
@@ -2518,10 +2532,6 @@ pub fn round(_value: f64, _precision: i64) -> f64 {
todo!()
}
-pub fn stdin_handle() -> PhpMixed {
- todo!()
-}
-
pub fn composer_dev_warning_time() -> i64 {
todo!()
}
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index 810e283..b97a5d5 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -5,21 +5,51 @@ use indexmap::IndexMap;
use shirabe_external_packages::composer::xdebug_handler::XdebugHandler;
use shirabe_external_packages::seld::json_lint::ParsingException;
-use shirabe_external_packages::symfony::console::Application as BaseApplication;
-use shirabe_external_packages::symfony::console::command::Command;
+use shirabe_external_packages::symfony::console::application::Application as BaseApplication;
+use shirabe_external_packages::symfony::console::command::Command as SymfonyCommand;
+use shirabe_external_packages::symfony::console::command::lazy_command::LazyCommand;
+use shirabe_external_packages::symfony::console::command::signalable_command_interface::SignalableCommandInterface;
+use shirabe_external_packages::symfony::console::command_loader::command_loader_interface::CommandLoaderInterface;
+use shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput;
+use shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions;
+use shirabe_external_packages::symfony::console::console_events::ConsoleEvents;
+use shirabe_external_packages::symfony::console::event::console_command_event::ConsoleCommandEvent;
+use shirabe_external_packages::symfony::console::event::console_error_event::ConsoleErrorEvent;
+use shirabe_external_packages::symfony::console::event::console_signal_event::ConsoleSignalEvent;
+use shirabe_external_packages::symfony::console::event::console_terminate_event::ConsoleTerminateEvent;
use shirabe_external_packages::symfony::console::exception::CommandNotFoundException;
use shirabe_external_packages::symfony::console::exception::ExceptionInterface;
+use shirabe_external_packages::symfony::console::exception::logic_exception::LogicException as ConsoleLogicException;
+use shirabe_external_packages::symfony::console::exception::namespace_not_found_exception::NamespaceNotFoundException;
+use shirabe_external_packages::symfony::console::exception::runtime_exception::RuntimeException as ConsoleRuntimeException;
+use shirabe_external_packages::symfony::console::formatter::output_formatter::OutputFormatter;
use shirabe_external_packages::symfony::console::helper::HelperInterface;
use shirabe_external_packages::symfony::console::helper::HelperSet;
use shirabe_external_packages::symfony::console::helper::HelperSetKey;
use shirabe_external_packages::symfony::console::helper::QuestionHelper;
+use shirabe_external_packages::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper;
+use shirabe_external_packages::symfony::console::helper::formatter_helper::{
+ FormatBlockMessages, FormatterHelper,
+};
+use shirabe_external_packages::symfony::console::helper::helper::Helper;
+use shirabe_external_packages::symfony::console::helper::process_helper::ProcessHelper;
use shirabe_external_packages::symfony::console::input::InputDefinition;
use shirabe_external_packages::symfony::console::input::InputInterface;
use shirabe_external_packages::symfony::console::input::InputOption;
+use shirabe_external_packages::symfony::console::input::argv_input::ArgvInput;
+use shirabe_external_packages::symfony::console::input::array_input::ArrayInput;
+use shirabe_external_packages::symfony::console::input::input_argument::InputArgument;
+use shirabe_external_packages::symfony::console::input::input_aware_interface::InputAwareInterface;
use shirabe_external_packages::symfony::console::output::ConsoleOutputInterface;
+use shirabe_external_packages::symfony::console::output::console_output::ConsoleOutput;
use shirabe_external_packages::symfony::console::output::output_interface::{
self as output_interface, OutputInterface,
};
+use shirabe_external_packages::symfony::console::signal_registry::signal_registry::SignalRegistry;
+use shirabe_external_packages::symfony::console::style::style_interface::StyleInterface;
+use shirabe_external_packages::symfony::console::style::symfony_style::SymfonyStyle;
+use shirabe_external_packages::symfony::console::terminal::Terminal;
+use shirabe_external_packages::symfony::contracts::event_dispatcher::event_dispatcher_interface::EventDispatcherInterface;
use shirabe_external_packages::symfony::process::exception::ProcessTimedOutException;
use shirabe_php_shim::{
LogicException as ShimLogicException, PHP_BINARY, PHP_VERSION, PHP_VERSION_ID, PhpMixed,
@@ -87,9 +117,29 @@ use crate::util::HttpDownloader;
use crate::util::Platform;
use crate::util::Silencer;
+/// The PHP `Composer\Console\Application` and `Symfony\Component\Console\Application` are
+/// flattened into a single struct. Methods that are overridden by subclass and called via
+/// `parent::` are prefixed by `base_`.
#[derive(Debug)]
pub struct Application {
- inner: BaseApplication,
+ commands: IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>,
+ want_helps: bool,
+ running_command: Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>,
+ name: String,
+ version: String,
+ command_loader: Option<Box<dyn CommandLoaderInterface>>,
+ catch_exceptions: bool,
+ auto_exit: bool,
+ definition: Option<std::rc::Rc<std::cell::RefCell<InputDefinition>>>,
+ helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>,
+ dispatcher: Option<std::rc::Rc<std::cell::RefCell<dyn EventDispatcherInterface>>>,
+ terminal: Terminal,
+ default_command: String,
+ single_command: bool,
+ initialized: bool,
+ signal_registry: Option<SignalRegistry>,
+ signals_to_dispatch_event: Vec<i64>,
+
pub(crate) composer: Option<PartialComposerHandle>,
pub(crate) io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
has_plugin_commands: bool,
@@ -103,11 +153,6 @@ impl Application {
const LOGO: &'static str = " ______\n / ____/___ ____ ___ ____ ____ ________ _____\n / / / __ \\/ __ `__ \\/ __ \\/ __ \\/ ___/ _ \\/ ___/\n/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ /\n\\____/\\____/_/ /_/ /_/ .___/\\____/____/\\___/_/\n /_/\n";
pub fn new(name: String, mut version: String) -> Self {
- // PHP: if (method_exists($this, 'setCatchErrors')) { $this->setCatchErrors(true); }
- // This Symfony Console port does not provide `setCatchErrors`, so the guarded call
- // is skipped, matching `method_exists` evaluating to false.
-
- // PHP: static $shutdownRegistered = false; — register only once globally
static SHUTDOWN_REGISTERED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if version == "" {
version = composer::get_version();
@@ -147,18 +192,41 @@ impl Application {
let initial_working_directory = getcwd();
- // PHP: parent::__construct($name, $version);
- let inner = BaseApplication::__construct(&name, &version);
-
- Self {
- inner,
+ let mut this = Self {
+ commands: IndexMap::new(),
+ want_helps: false,
+ running_command: None,
+ name,
+ version,
+ command_loader: None,
+ catch_exceptions: true,
+ auto_exit: true,
+ definition: None,
+ helper_set: None,
+ dispatcher: None,
+ terminal: Terminal::new(),
+ default_command: "list".to_string(),
+ single_command: false,
+ initialized: false,
+ signal_registry: None,
+ signals_to_dispatch_event: Vec::new(),
composer: None,
io,
has_plugin_commands: false,
disable_plugins_by_default: false,
disable_scripts_by_default: false,
initial_working_directory,
+ };
+ if defined("SIGINT") && SignalRegistry::is_supported() {
+ this.signal_registry = Some(SignalRegistry::new());
+ this.signals_to_dispatch_event = vec![
+ shirabe_php_shim::SIGINT,
+ shirabe_php_shim::SIGTERM,
+ shirabe_php_shim::SIGUSR1,
+ shirabe_php_shim::SIGUSR2,
+ ];
}
+ this
}
pub fn run(
@@ -174,7 +242,7 @@ impl Application {
),
};
- self.inner.run(input, output)
+ self.base_run(input, output)
}
pub fn do_run(
@@ -189,26 +257,14 @@ impl Application {
.borrow()
.has_parameter_option(PhpMixed::from(vec!["--no-scripts"]), false);
- // PHP: static $stdin = null; — cached across doRun calls so the php://stdin handle is
- // opened once.
- // TODO(phase-c): faithfully caching the stdin resource across calls needs a real
- // file-handle/resource model (PhpMixed currently boxes the handle opaquely) plus a
- // function-static store. Recomputing per call is observably equivalent for a single
- // invocation but differs if doRun is re-entered; deferred until the resource model lands.
- let stdin: PhpMixed = if defined("STDIN") {
- shirabe_php_shim::stdin_handle()
- } else {
- shirabe_php_shim::fopen("php://stdin", "r")
- };
+ let stdin = shirabe_php_shim::STDIN;
if Platform::get_env("COMPOSER_TESTS_ARE_RUNNING").as_deref() != Some("1")
&& (Platform::get_env("COMPOSER_NO_INTERACTION").is_some()
- || matches!(stdin, PhpMixed::Null)
|| !Platform::is_tty(Some(stdin)))
{
input.borrow_mut().set_interactive(false);
}
- // PHP: $this->io = new ConsoleIO($input, $output, new HelperSet([new QuestionHelper()]));
let mut helpers: IndexMap<
HelperSetKey,
std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>,
@@ -270,7 +326,7 @@ impl Application {
let mut command_name: Option<String> = Some(String::new());
let raw_command_name = self.get_command_name_before_binding(input.clone());
if let Some(ref raw) = raw_command_name {
- match self.inner.find(raw) {
+ match self.find(raw) {
Ok(cmd) => {
// TODO(phase-c): the Symfony Application stub keeps its command registry as
// PhpMixed with a todo!() find(), per the "Symfony stays todo!()" policy.
@@ -460,7 +516,7 @@ impl Application {
match (|| -> anyhow::Result<()> {
for command in self.get_plugin_commands()? {
let cmd_name = command.get_name().unwrap_or_default();
- if self.inner.has(&cmd_name) {
+ if self.has(&cmd_name) {
// TODO(plugin): PHP uses get_class($command) for the skipped-command class
// name. Plugin command discovery (get_plugin_commands) is unimplemented, so
// this loop never runs; wire the concrete class name with the plugin API.
@@ -512,7 +568,7 @@ impl Application {
// determine command name to be executed incl plugin commands, and check if it's a proxy command
let is_proxy_command = false;
if let Some(ref name) = self.get_command_name_before_binding(input.clone()) {
- if let Ok(command) = self.inner.find(name) {
+ if let Ok(command) = self.find(name) {
// TODO(phase-c): same blocker as the earlier find() call — the Symfony command
// registry is a PhpMixed/todo!() stub, so the resolved command's name and its
// isProxyCommand() flag cannot be recovered until the typed-command registry is
@@ -625,7 +681,7 @@ impl Application {
str_replace("-", "_", &strtoupper(script))
);
if !defined(&script_event_const) {
- if self.inner.has(script) {
+ if self.has(script) {
self.io.write_error(&format!("<warning>A script named {} would override a Composer command and has been skipped</warning>", script));
} else {
let mut description = format!(
@@ -687,7 +743,7 @@ impl Application {
loader.register(false);
}
- // if the command is not an array of commands, and points to a valid Command subclass, import its details directly
+ // if the command is not an array of commands, and points to a valid SymfonyCommand subclass, import its details directly
let dummy_str = dummy.as_string().unwrap_or("").to_string();
let cmd: PhpMixed = if is_string(dummy)
&& shirabe_php_shim::class_exists(&dummy_str)
@@ -709,14 +765,14 @@ impl Application {
);
// TODO(phase-c): the script's command class is built by
// reflection (instantiate_class) and stays PhpMixed; the
- // SingleCommandApplication / Command typed registry it
+ // SingleCommandApplication / SymfonyCommand typed registry it
// belongs to is an external-package todo!() stub.
// let _ = SingleCommandApplication::new;
// makes sure the command is find()'able by the name defined in composer.json, and the name isn't overridden in its configure()
// TODO(phase-c): cmd is the PhpMixed result of reflection
// instantiation; reading/overriding its
- // name/description requires the typed Command model that
+ // name/description requires the typed SymfonyCommand model that
// the Symfony stub does not yet provide.
let _ = description.clone();
let _ = &mut cmd;
@@ -736,7 +792,8 @@ impl Application {
};
// Compatibility layer for symfony/console <7.4
- // TODO(phase-c): self.inner.add() takes Rc<RefCell<dyn Command>>
+ // TODO(phase-c): Application::add() takes Rc<RefCell<dyn
+ // SymfonyCommand>>
// but `cmd` here is the PhpMixed result of reflection-based
// plugin command instantiation; registering it as a typed
// command instance is blocked on the Symfony command-registry
@@ -768,7 +825,7 @@ impl Application {
let _ = start_time.unwrap();
}
- let result: i64 = self.inner.do_run(input.clone(), output.clone())?;
+ let result: i64 = self.base_do_run(input.clone(), output.clone())?;
if input
.borrow()
@@ -783,28 +840,28 @@ impl Application {
);
}
- Ok(result)
- })();
+ // chdir back to oldWorkingDir if set
+ if let Some(ref owd) = old_working_dir {
+ if !owd.is_empty() {
+ let owd = owd.clone();
+ let _ = Silencer::call(|| {
+ chdir(&owd);
+ Ok(())
+ });
+ }
+ }
- // chdir back to oldWorkingDir if set — runs regardless of result
- if let Some(ref owd) = old_working_dir {
- if !owd.is_empty() {
- let owd = owd.clone();
- let _ = Silencer::call(|| {
- chdir(&owd);
- Ok(())
- });
+ if let Some(st) = start_time {
+ self.io.write_error(&format!(
+ "<info>Memory usage: {}MiB (peak: {}MiB), time: {}s</info>",
+ round((memory_get_usage() as f64) / 1024.0 / 1024.0, 2),
+ round((memory_get_peak_usage(false) as f64) / 1024.0 / 1024.0, 2),
+ round(microtime(true) - st, 2)
+ ));
}
- }
- if let Some(st) = start_time {
- self.io.write_error(&format!(
- "<info>Memory usage: {}MiB (peak: {}MiB), time: {}s</info>",
- round((memory_get_usage() as f64) / 1024.0 / 1024.0, 2),
- round((memory_get_peak_usage(true) as f64) / 1024.0 / 1024.0, 2),
- round(microtime(true) - st, 2)
- ));
- }
+ Ok(result)
+ })();
let outcome = match result_outcome {
Ok(r) => Ok(r),
@@ -1059,7 +1116,7 @@ impl Application {
} else {
if required {
self.io.write_error(&e.to_string());
- if self.inner.are_exceptions_caught() {
+ if self.are_exceptions_caught() {
std::process::exit(1);
}
return Err(e);
@@ -1081,27 +1138,25 @@ impl Application {
}
}
- /// Delegates to the underlying BaseApplication's `find` method (PHP Symfony Console).
- pub fn find(&self, _name: &str) -> anyhow::Result<shirabe_php_shim::PhpMixed> {
- todo!()
- }
-
pub fn get_io(&self) -> std::rc::Rc<std::cell::RefCell<dyn IOInterface>> {
self.io.clone()
}
pub fn get_help(&self) -> String {
- format!("{}{}", Self::LOGO, self.inner.get_help())
+ format!("{}{}", Self::LOGO, self.base_get_help())
}
/// Initializes all the composer commands.
- pub(crate) fn get_default_commands(&self) -> Vec<Box<dyn Command>> {
+ pub(crate) fn get_default_commands(
+ &self,
+ ) -> Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
// PHP: array_merge(parent::getDefaultCommands(), [new AboutCommand(), ...]).
// TODO(phase-c): the composer commands implement the shirabe BaseCommand trait, not the
- // Symfony Command trait, and the orphan rule forbids a blanket
- // `impl<C: HasBaseCommandData> Command for C`. Each command needs its own `impl Command`
- // (or a wrapper) before they can be returned as `Box<dyn Command>`; the parent list is
- // likewise a Symfony stub (get_default_commands returns Vec<PhpMixed>/todo!()).
+ // Symfony SymfonyCommand trait, and the orphan rule forbids a blanket
+ // `impl<C: HasBaseCommandData> SymfonyCommand for C`. Each command needs its own `impl
+ // SymfonyCommand`
+ // (or a wrapper) before they can be merged with `base_get_default_commands()` and returned;
+ // the parent list (`base_get_default_commands`) is likewise a stub.
vec![]
}
@@ -1133,15 +1188,15 @@ impl Application {
format!(
"<info>{}</info> version <comment>{}{}</comment> {}",
- self.inner.get_name(),
- self.inner.get_version(),
+ self.get_name(),
+ self.get_version(),
branch_alias_string,
composer::RELEASE_DATE,
)
}
pub(crate) fn get_default_input_definition(&self) -> anyhow::Result<InputDefinition> {
- let mut definition = self.inner.get_default_input_definition();
+ let mut definition = self.base_get_default_input_definition();
definition.add_option(InputOption::new(
"--profile",
PhpMixed::Null,
@@ -1181,9 +1236,9 @@ impl Application {
Ok(definition)
}
- fn get_plugin_commands(&mut self) -> anyhow::Result<Vec<Box<dyn Command>>> {
+ fn get_plugin_commands(&mut self) -> anyhow::Result<Vec<Box<dyn SymfonyCommand>>> {
// TODO(plugin): plugin command discovery is part of the plugin API
- let commands: Vec<Box<dyn Command>> = vec![];
+ let commands: Vec<Box<dyn SymfonyCommand>> = vec![];
// TODO(phase-c): discovering plugin-provided commands walks the PluginManager and
// downcasts each plugin's CommandProvider capability — this is the Plugin API surface,
@@ -1228,3 +1283,1675 @@ impl Application {
function_exists("posix_getuid") && posix_getuid() == 0
}
}
+
+/// Methods inherited from `Symfony\Component\Console\Application`. They live in the same `impl`
+/// surface as the Composer overrides above so that polymorphic `self.*` calls dispatch to the
+/// Composer version when one exists. Methods that Composer overrides while still calling `parent::`
+/// are carried here under a `base_` prefix (`base_run`, `base_do_run`, `base_get_help`,
+/// `base_get_default_input_definition`, `base_get_default_commands`).
+impl Application {
+ /// @final
+ pub fn set_dispatcher(
+ &mut self,
+ dispatcher: std::rc::Rc<std::cell::RefCell<dyn EventDispatcherInterface>>,
+ ) {
+ // TODO(plugin): the event dispatcher drives ConsoleEvents listeners (plugins).
+ self.dispatcher = Some(dispatcher);
+ }
+
+ pub fn set_command_loader(&mut self, command_loader: Box<dyn CommandLoaderInterface>) {
+ self.command_loader = Some(command_loader);
+ }
+
+ pub fn get_signal_registry(&self) -> anyhow::Result<&SignalRegistry> {
+ match &self.signal_registry {
+ None => Err(ConsoleRuntimeException(shirabe_php_shim::RuntimeException {
+ message: "Signals are not supported. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(),
+ code: 0,
+ })
+ .into()),
+ Some(signal_registry) => Ok(signal_registry),
+ }
+ }
+
+ pub fn set_signals_to_dispatch_event(&mut self, signals_to_dispatch_event: Vec<i64>) {
+ self.signals_to_dispatch_event = signals_to_dispatch_event;
+ }
+
+ /// Runs the current application (Symfony base; `parent::run`).
+ pub fn base_run(
+ &mut self,
+ input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>,
+ output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+ ) -> anyhow::Result<i64> {
+ if shirabe_php_shim::function_exists("putenv") {
+ shirabe_php_shim::putenv(&format!("LINES={}", self.terminal.get_height()));
+ shirabe_php_shim::putenv(&format!("COLUMNS={}", self.terminal.get_width()));
+ }
+
+ let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = match input {
+ None => std::rc::Rc::new(std::cell::RefCell::new(ArgvInput::new(None, None)?)),
+ Some(input) => input,
+ };
+
+ let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = match output {
+ None => std::rc::Rc::new(std::cell::RefCell::new(ConsoleOutput::new(
+ None, None, None,
+ )?)),
+ Some(output) => output,
+ };
+
+ // TODO: PHP installs a temporary `set_exception_handler($renderException)` and cooperates
+ // with Symfony's ErrorHandler to keep/restore it. PHP's process-global exception handler
+ // stack has no Rust equivalent; the rendering itself is invoked directly in the catch
+ // branch below. Review needed for the handler save/restore dance.
+ let render_exception =
+ |this: &Application,
+ e: &anyhow::Error,
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| {
+ // if ($output instanceof ConsoleOutputInterface) render to its error output
+ // TODO(review): downcasting a `dyn OutputInterface` to `ConsoleOutputInterface`
+ // is not directly expressible; the ConsoleOutputInterface branch needs design.
+ this.render_throwable(e, output.clone());
+ };
+
+ let result = (|| -> anyhow::Result<i64> {
+ self.configure_io(&input, &output)?;
+
+ let exit_code = self.do_run(input.clone(), output.clone())?;
+
+ Ok(exit_code)
+ })();
+
+ let mut exit_code = match result {
+ Ok(exit_code) => exit_code,
+ Err(e) => {
+ if !self.catch_exceptions {
+ return Err(e);
+ }
+
+ render_exception(self, &e, &output);
+
+ // $exitCode = $e->getCode();
+ // is_numeric($exitCode) ? max(1, (int) $exitCode) : 1
+ // TODO(review): anyhow::Error has no PHP-style getCode(); the exit code derived
+ // from the exception's `code` field needs the downcast strategy decided.
+ let exit_code = shirabe_php_shim::php_exception_get_code(&e);
+ if shirabe_php_shim::is_numeric_string(&exit_code.to_string()) {
+ let exit_code = exit_code;
+ if exit_code <= 0 { 1 } else { exit_code }
+ } else {
+ 1
+ }
+ }
+ };
+
+ // finally: handler restore. See TODO above; no-op here.
+
+ if self.auto_exit {
+ if exit_code > 255 {
+ exit_code = 255;
+ }
+
+ shirabe_php_shim::exit(exit_code);
+ }
+
+ Ok(exit_code)
+ }
+
+ /// Runs the current application (Symfony base; `parent::doRun`).
+ pub fn base_do_run(
+ &mut self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![
+ PhpMixed::from("--version".to_string()),
+ PhpMixed::from("-V".to_string()),
+ ]),
+ true,
+ ) {
+ output
+ .borrow()
+ .writeln(&[self.get_long_version()], output_interface::OUTPUT_NORMAL);
+
+ return Ok(0);
+ }
+
+ // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
+ match input.borrow_mut().bind(&self.get_definition().borrow()) {
+ Ok(()) => {}
+ Err(e) => {
+ // Errors must be ignored, full binding/validation happens later when the command is known.
+ if !is_exception_interface(&e) {
+ return Err(e);
+ }
+ }
+ }
+
+ let mut input = input;
+ let mut name = self.get_command_name(&*input.borrow());
+ if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![
+ PhpMixed::from("--help".to_string()),
+ PhpMixed::from("-h".to_string()),
+ ]),
+ true,
+ ) {
+ if name.is_none() {
+ name = Some("help".to_string());
+ input = std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(
+ vec![(
+ PhpMixed::from("command_name".to_string()),
+ PhpMixed::from(self.default_command.clone()),
+ )],
+ None,
+ )?));
+ } else {
+ self.want_helps = true;
+ }
+ }
+
+ let name = match name {
+ Some(name) => name,
+ None => {
+ let name = self.default_command.clone();
+ let definition = self.get_definition();
+ let command_description = definition
+ .borrow()
+ .get_argument(&PhpMixed::from("command".to_string()))?
+ .get_description()
+ .to_string();
+ let _new_command_argument = InputArgument::new(
+ "command".to_string(),
+ Some(InputArgument::OPTIONAL),
+ command_description,
+ PhpMixed::from(name.clone()),
+ )?;
+ // $definition->setArguments(array_merge($definition->getArguments(),
+ // ['command' => new InputArgument('command', InputArgument::OPTIONAL, ...)]))
+ // TODO(review): get_arguments() yields Rc<InputArgument> (shared, non-Clone) while
+ // set_arguments() consumes owned InputArgument values. Re-building the merged
+ // argument list requires an InputArgument clone/ownership strategy not yet present.
+ definition.borrow_mut().set_arguments(todo!(
+ "merge existing arguments with the new 'command' argument"
+ ))?;
+
+ name
+ }
+ };
+
+ let command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>;
+ let find_result =
+ (|| -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
+ self.running_command = None;
+ // the command name MUST be the first element of the input
+ self.find(&name)
+ })();
+
+ match find_result {
+ Ok(c) => {
+ command = c;
+ }
+ Err(e) => {
+ // if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException)
+ // || 1 !== count($alternatives = $e->getAlternatives()) || !$input->isInteractive())
+ let alternatives: Option<Vec<String>> = downcast_command_not_found(&e)
+ .filter(|_| !is_namespace_not_found(&e))
+ .map(|cnf| cnf.get_alternatives().clone());
+
+ let single_alternative = match &alternatives {
+ Some(alts) if alts.len() == 1 => Some(alts[0].clone()),
+ _ => None,
+ };
+
+ if single_alternative.is_none() || !input.borrow().is_interactive() {
+ let mut e = e;
+ if self.dispatcher.is_some() {
+ // TODO(plugin): dispatch ConsoleErrorEvent so listeners can handle/replace the error.
+ let _event = ConsoleErrorEvent::new(
+ todo!("wrap input as Box<dyn InputInterface> for the event"),
+ todo!("wrap output as Box<dyn OutputInterface> for the event"),
+ todo!("wrap anyhow::Error as Box<dyn Error> for the event"),
+ None,
+ );
+ let event: ConsoleErrorEvent = _event;
+ self.dispatcher
+ .as_ref()
+ .unwrap()
+ .borrow_mut()
+ .dispatch(todo!("event object"), ConsoleEvents::ERROR);
+
+ if event.get_exit_code() == 0 {
+ return Ok(0);
+ }
+
+ e = todo!("event.get_error() converted back to anyhow::Error");
+ }
+
+ return Err(e);
+ }
+
+ let alternative = single_alternative.unwrap();
+
+ let mut style = SymfonyStyle::new(input.clone(), output.clone());
+ output
+ .borrow()
+ .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL);
+ let formatted_block = FormatterHelper::default().format_block(
+ FormatBlockMessages::String(format!(
+ "Command \"{}\" is not defined.",
+ PhpMixed::from(name.clone()),
+ )),
+ "error",
+ true,
+ );
+ output
+ .borrow()
+ .writeln(&[formatted_block], output_interface::OUTPUT_NORMAL);
+ if !style.confirm(
+ &format!(
+ "Do you want to run \"{}\" instead? ",
+ PhpMixed::from(alternative.clone()),
+ ),
+ false,
+ ) {
+ if self.dispatcher.is_some() {
+ // TODO(plugin): dispatch ConsoleErrorEvent for the declined-alternative case.
+ let event = ConsoleErrorEvent::new(
+ todo!("wrap input as Box<dyn InputInterface>"),
+ todo!("wrap output as Box<dyn OutputInterface>"),
+ todo!("wrap error as Box<dyn Error>"),
+ None,
+ );
+ self.dispatcher
+ .as_ref()
+ .unwrap()
+ .borrow_mut()
+ .dispatch(todo!("event object"), ConsoleEvents::ERROR);
+
+ return Ok(event.get_exit_code());
+ }
+
+ return Ok(1);
+ }
+
+ command = self.find(&alternative)?;
+ }
+ }
+
+ // if ($command instanceof LazyCommand) $command = $command->getCommand();
+ // TODO(review): LazyCommand is a distinct type from SymfonyCommand here; PHP unwraps the real
+ // command. The `commands` map stores Rc<RefCell<dyn SymfonyCommand>>, so the LazyCommand-unwrap path
+ // needs a design decision about how lazy commands are represented.
+ let _ = std::marker::PhantomData::<LazyCommand>;
+
+ self.running_command = Some(command.clone());
+ let exit_code = self.do_run_command(command.clone(), input.clone(), output.clone())?;
+ self.running_command = None;
+
+ Ok(exit_code)
+ }
+
+ pub fn set_helper_set(&mut self, helper_set: std::rc::Rc<std::cell::RefCell<HelperSet>>) {
+ self.helper_set = Some(helper_set);
+ }
+
+ /// Get the helper set associated with the command.
+ pub fn get_helper_set(&mut self) -> std::rc::Rc<std::cell::RefCell<HelperSet>> {
+ if self.helper_set.is_none() {
+ self.helper_set = Some(self.get_default_helper_set());
+ }
+
+ self.helper_set.as_ref().unwrap().clone()
+ }
+
+ pub fn set_definition(&mut self, definition: std::rc::Rc<std::cell::RefCell<InputDefinition>>) {
+ self.definition = Some(definition);
+ }
+
+ /// Gets the InputDefinition related to this Application.
+ pub fn get_definition(&mut self) -> std::rc::Rc<std::cell::RefCell<InputDefinition>> {
+ if self.definition.is_none() {
+ // `get_default_input_definition` is the Composer override (returns a Result because the
+ // Rust `InputOption::new` is fallible); the option modes are constants that cannot fail,
+ // so unwrapping mirrors the PHP call that never throws here.
+ self.definition = Some(std::rc::Rc::new(std::cell::RefCell::new(
+ self.get_default_input_definition().unwrap(),
+ )));
+ }
+
+ if self.single_command {
+ let input_definition = self.definition.as_ref().unwrap().clone();
+ input_definition
+ .borrow_mut()
+ .set_arguments(Vec::new())
+ .unwrap();
+
+ return input_definition;
+ }
+
+ self.definition.as_ref().unwrap().clone()
+ }
+
+ /// Adds suggestions to `suggestions` for the current completion input (e.g. option or argument).
+ pub fn complete(
+ &mut self,
+ input: &CompletionInput,
+ suggestions: &mut CompletionSuggestions,
+ ) -> anyhow::Result<()> {
+ if CompletionInput::TYPE_ARGUMENT_VALUE == input.get_completion_type()
+ && input.get_completion_name().as_deref() == Some("command")
+ {
+ let mut command_names: Vec<PhpMixed> = Vec::new();
+ for (name, command) in self.all(None)? {
+ // skip hidden commands and aliased commands as they already get added below
+ if command.borrow().is_hidden() || command.borrow().get_name() != Some(name.clone())
+ {
+ continue;
+ }
+ command_names.push(PhpMixed::from(
+ command.borrow().get_name().unwrap_or_default(),
+ ));
+ for name in command.borrow().get_aliases() {
+ command_names.push(PhpMixed::from(name));
+ }
+ }
+ // array_filter($commandNames)
+ let filtered: Vec<shirabe_external_packages::symfony::console::completion::completion_suggestions::StringOrSuggestion> =
+ command_names
+ .into_iter()
+ .filter(|n| shirabe_php_shim::php_truthy(n))
+ .map(|n| {
+ shirabe_external_packages::symfony::console::completion::completion_suggestions::StringOrSuggestion::String(
+ shirabe_php_shim::php_to_string(&n),
+ )
+ })
+ .collect();
+ suggestions.suggest_values(filtered);
+
+ return Ok(());
+ }
+
+ if CompletionInput::TYPE_OPTION_NAME == input.get_completion_type() {
+ // $suggestions->suggestOptions($this->getDefinition()->getOptions());
+ // TODO(review): get_options() yields Rc<InputOption> (shared, non-Clone) while
+ // suggest_options() consumes owned InputOption values; an ownership/clone strategy
+ // for InputOption is needed.
+ suggestions.suggest_options(todo!("owned options from get_definition().get_options()"));
+
+ return Ok(());
+ }
+
+ Ok(())
+ }
+
+ /// Gets the help message (Symfony base; `parent::getHelp`).
+ pub fn base_get_help(&self) -> String {
+ self.get_long_version()
+ }
+
+ /// Gets whether to catch exceptions or not during commands execution.
+ pub fn are_exceptions_caught(&self) -> bool {
+ self.catch_exceptions
+ }
+
+ /// Sets whether to catch exceptions or not during commands execution.
+ pub fn set_catch_exceptions(&mut self, boolean: bool) {
+ self.catch_exceptions = boolean;
+ }
+
+ /// Gets whether to automatically exit after a command execution or not.
+ pub fn is_auto_exit_enabled(&self) -> bool {
+ self.auto_exit
+ }
+
+ /// Sets whether to automatically exit after a command execution or not.
+ pub fn set_auto_exit(&mut self, boolean: bool) {
+ self.auto_exit = boolean;
+ }
+
+ /// Gets the name of the application.
+ pub fn get_name(&self) -> String {
+ self.name.clone()
+ }
+
+ /// Sets the application name.
+ pub fn set_name(&mut self, name: &str) {
+ self.name = name.to_string();
+ }
+
+ /// Gets the application version.
+ pub fn get_version(&self) -> String {
+ self.version.clone()
+ }
+
+ /// Sets the application version.
+ pub fn set_version(&mut self, version: &str) {
+ self.version = version.to_string();
+ }
+
+ /// Adds an array of command objects.
+ ///
+ /// If a SymfonyCommand is not enabled it will not be added.
+ pub fn add_commands(
+ &mut self,
+ commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>,
+ ) -> anyhow::Result<()> {
+ for command in commands {
+ self.add(command)?;
+ }
+ Ok(())
+ }
+
+ /// Adds a command object.
+ ///
+ /// If a command with the same name already exists, it will be overridden.
+ /// If the command is not enabled it will not be added.
+ pub fn add(
+ &mut self,
+ command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
+ ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
+ self.init()?;
+
+ // TODO(review): $command->setApplication($this) needs an Rc<RefCell<Application>> to the
+ // current instance. Application is held by value here; the self-reference required to set
+ // the command's back-pointer needs the shared-ownership design (Phase C).
+ command
+ .borrow_mut()
+ .set_application(todo!("Rc<RefCell<Application>> of self"));
+
+ if !command.borrow().is_enabled() {
+ command.borrow_mut().set_application(None);
+
+ return Ok(None);
+ }
+
+ // if (!$command instanceof LazyCommand) { $command->getDefinition(); }
+ // TODO(review): LazyCommand vs SymfonyCommand type distinction; eager definition probe omitted
+ // pending lazy-command representation decision.
+ command.borrow().get_definition();
+
+ if command.borrow().get_name().is_none() {
+ return Err(ConsoleLogicException(shirabe_php_shim::LogicException {
+ message: format!(
+ "The command defined in \"{}\" cannot have an empty name.",
+ PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command,)),
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ let name = command.borrow().get_name().unwrap();
+ self.commands.insert(name, command.clone());
+
+ for alias in command.borrow().get_aliases() {
+ self.commands.insert(alias, command.clone());
+ }
+
+ Ok(Some(command))
+ }
+
+ /// Returns a registered command by name or alias.
+ ///
+ /// Throws CommandNotFoundException when given command name does not exist.
+ pub fn get(
+ &mut self,
+ name: &str,
+ ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
+ self.init()?;
+
+ if !self.has(name) {
+ return Err(CommandNotFoundException::new(
+ format!(
+ "The command \"{}\" does not exist.",
+ PhpMixed::from(name.to_string()),
+ ),
+ Vec::new(),
+ 0,
+ )
+ .into());
+ }
+
+ // When the command has a different name than the one used at the command loader level
+ if !self.commands.contains_key(name) {
+ return Err(CommandNotFoundException::new(
+ format!(
+ "The \"{}\" command cannot be found because it is registered under multiple names. Make sure you don't set a different name via constructor or \"setName()\".",
+ PhpMixed::from(name.to_string()),
+ ),
+ Vec::new(),
+ 0,
+ )
+ .into());
+ }
+
+ let command = self.commands[name].clone();
+
+ if self.want_helps {
+ self.want_helps = false;
+
+ let help_command = self.get("help")?;
+ // $helpCommand->setCommand($command);
+ // TODO(review): setCommand() is defined on HelpCommand, not on the concrete
+ // `SymfonyCommand`
+ // struct; calling it through the Rc<RefCell<dyn SymfonyCommand>> needs the
+ // SymfonyCommand-subclass
+ // representation decision (downcast to HelpCommand).
+ let _ = &command;
+ todo!("help_command.set_command(command)");
+
+ #[allow(unreachable_code)]
+ return Ok(help_command);
+ }
+
+ Ok(command)
+ }
+
+ /// Returns true if the command exists, false otherwise.
+ pub fn has(&mut self, name: &str) -> bool {
+ self.init().unwrap();
+
+ if self.commands.contains_key(name) {
+ return true;
+ }
+
+ if let Some(command_loader) = &self.command_loader {
+ if command_loader.has(name) {
+ let command = command_loader.get(name);
+ // $this->add($this->commandLoader->get($name))
+ // TODO(review): command_loader.get() returns Box<dyn SymfonyCommand> while add() expects
+ // Rc<RefCell<dyn SymfonyCommand>>; the loader return type needs reconciliation.
+ let _ = command;
+ return self
+ .add(todo!(
+ "Rc<RefCell<dyn SymfonyCommand>> from command_loader.get(name)"
+ ))
+ .map(|c| c.is_some())
+ .unwrap_or(false);
+ }
+ }
+
+ false
+ }
+
+ /// Returns an array of all unique namespaces used by currently registered commands.
+ ///
+ /// It does not return the global namespace which always exists.
+ pub fn get_namespaces(&mut self) -> anyhow::Result<Vec<String>> {
+ let mut namespaces: Vec<Vec<String>> = Vec::new();
+ for command in self.all(None)?.values() {
+ if command.borrow().is_hidden() {
+ continue;
+ }
+
+ namespaces.push(
+ self.extract_all_namespaces(&command.borrow().get_name().unwrap_or_default()),
+ );
+
+ for alias in command.borrow().get_aliases() {
+ namespaces.push(self.extract_all_namespaces(&alias));
+ }
+ }
+
+ // array_values(array_unique(array_filter(array_merge([], ...$namespaces))))
+ let mut merged: Vec<String> = Vec::new();
+ for ns in namespaces {
+ merged.extend(ns);
+ }
+ let merged: Vec<String> = merged.into_iter().filter(|s| !s.is_empty()).collect();
+ let mut seen = std::collections::HashSet::new();
+ let unique: Vec<String> = merged
+ .into_iter()
+ .filter(|s| seen.insert(s.clone()))
+ .collect();
+
+ Ok(unique)
+ }
+
+ /// Finds a registered namespace by a name or an abbreviation.
+ ///
+ /// Throws NamespaceNotFoundException when namespace is incorrect or ambiguous.
+ pub fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String> {
+ let all_namespaces = self.get_namespaces()?;
+ // implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'
+ let parts: Vec<String> = shirabe_php_shim::explode(":", namespace)
+ .into_iter()
+ .map(|p| shirabe_php_shim::preg_quote(&p, None))
+ .collect();
+ let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*");
+ let namespaces = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_namespaces);
+
+ if namespaces.is_empty() {
+ let mut message = format!(
+ "There are no commands defined in the \"{}\" namespace.",
+ PhpMixed::from(namespace.to_string()),
+ );
+
+ let alternatives = self.find_alternatives(namespace, &all_namespaces);
+ if !alternatives.is_empty() {
+ if alternatives.len() == 1 {
+ message.push_str("\n\nDid you mean this?\n ");
+ } else {
+ message.push_str("\n\nDid you mean one of these?\n ");
+ }
+
+ message.push_str(&shirabe_php_shim::implode("\n ", &alternatives));
+ }
+
+ return Err(NamespaceNotFoundException(CommandNotFoundException::new(
+ message,
+ alternatives,
+ 0,
+ ))
+ .into());
+ }
+
+ let exact = namespaces.iter().any(|n| n == namespace);
+ if namespaces.len() > 1 && !exact {
+ return Err(NamespaceNotFoundException(CommandNotFoundException::new(
+ format!(
+ "The namespace \"{}\" is ambiguous.\nDid you mean one of these?\n{}.",
+ PhpMixed::from(namespace.to_string()),
+ PhpMixed::from(self.get_abbreviation_suggestions(&namespaces)),
+ ),
+ namespaces.clone(),
+ 0,
+ ))
+ .into());
+ }
+
+ // $exact ? $namespace : reset($namespaces)
+ if exact {
+ Ok(namespace.to_string())
+ } else {
+ Ok(namespaces[0].clone())
+ }
+ }
+
+ /// Finds a command by name or alias.
+ ///
+ /// Contrary to get, this command tries to find the best match if you give it an
+ /// abbreviation of a name or alias.
+ ///
+ /// Throws CommandNotFoundException when command name is incorrect or ambiguous.
+ pub fn find(
+ &mut self,
+ name: &str,
+ ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
+ self.init()?;
+
+ let mut aliases: IndexMap<String, String> = IndexMap::new();
+
+ let commands_snapshot: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> =
+ self.commands.values().cloned().collect();
+ for command in &commands_snapshot {
+ for alias in command.borrow().get_aliases() {
+ if !self.has(&alias) {
+ self.commands.insert(alias, command.clone());
+ }
+ }
+ }
+
+ if self.has(name) {
+ return self.get(name);
+ }
+
+ // $allCommands = commandLoader ? array_merge(loader->getNames(), array_keys(commands)) : array_keys(commands)
+ let all_commands: Vec<String> = match &self.command_loader {
+ Some(command_loader) => {
+ let mut all = command_loader.get_names();
+ all.extend(self.commands.keys().cloned());
+ all
+ }
+ None => self.commands.keys().cloned().collect(),
+ };
+
+ let parts: Vec<String> = shirabe_php_shim::explode(":", name)
+ .into_iter()
+ .map(|p| shirabe_php_shim::preg_quote(&p, None))
+ .collect();
+ let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*");
+ let mut commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_commands);
+
+ if commands.is_empty() {
+ commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}i", expr), &all_commands);
+ }
+
+ // if no commands matched or we just matched namespaces
+ if commands.is_empty()
+ || shirabe_php_shim::preg_grep(&format!("{{^{}$}}i", expr), &commands).len() < 1
+ {
+ if let Some(pos) = shirabe_php_shim::strrpos(name, ":") {
+ // check if a namespace exists and contains commands
+ self.find_namespace(&name[..pos as usize])?;
+ }
+
+ let mut message = format!(
+ "SymfonyCommand \"{}\" is not defined.",
+ PhpMixed::from(name.to_string()),
+ );
+
+ let mut alternatives = self.find_alternatives(name, &all_commands);
+ if !alternatives.is_empty() {
+ // remove hidden commands
+ let mut filtered: Vec<String> = Vec::new();
+ for alt in alternatives {
+ if !self.get(&alt)?.borrow().is_hidden() {
+ filtered.push(alt);
+ }
+ }
+ alternatives = filtered;
+
+ if alternatives.len() == 1 {
+ message.push_str("\n\nDid you mean this?\n ");
+ } else {
+ message.push_str("\n\nDid you mean one of these?\n ");
+ }
+ message.push_str(&shirabe_php_shim::implode("\n ", &alternatives));
+ }
+
+ return Err(CommandNotFoundException::new(message, alternatives, 0).into());
+ }
+
+ // filter out aliases for commands which are already on the list
+ if commands.len() > 1 {
+ // $commandList = commandLoader ? array_merge(array_flip(loader->getNames()), commands) : commands
+ // TODO(review): $commandList mixes flipped loader names (string => int) with
+ // SymfonyCommand
+ // instances; this heterogeneous PHP array needs a typed representation. The alias
+ // de-duplication and the loader->get() lazy materialization are left to design.
+ let mut command_list: IndexMap<
+ String,
+ std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
+ > = self.commands.clone();
+
+ let commands_clone = commands.clone();
+ let mut new_commands: Vec<String> = Vec::new();
+ let mut seen = std::collections::HashSet::new();
+ for name_or_alias in commands {
+ if !command_list.contains_key(&name_or_alias) {
+ let loaded = self.command_loader.as_ref().unwrap().get(&name_or_alias);
+ let _ = loaded;
+ command_list.insert(
+ name_or_alias.clone(),
+ todo!(
+ "Rc<RefCell<dyn SymfonyCommand>> from command_loader.get(name_or_alias)"
+ ),
+ );
+ }
+
+ let command_name = command_list[&name_or_alias]
+ .borrow()
+ .get_name()
+ .unwrap_or_default();
+
+ aliases.insert(name_or_alias.clone(), command_name.clone());
+
+ let keep = command_name == name_or_alias || !commands_clone.contains(&command_name);
+ if keep && seen.insert(name_or_alias.clone()) {
+ new_commands.push(name_or_alias);
+ }
+ }
+ commands = new_commands;
+
+ if commands.len() > 1 {
+ let usable_width = self.terminal.get_width() - 10;
+ let abbrevs: Vec<String> = commands.clone();
+ let mut max_len: i64 = 0;
+ for abbrev in &abbrevs {
+ max_len = std::cmp::max(Helper::width(abbrev), max_len);
+ }
+ let mut formatted_abbrevs: Vec<PhpMixed> = Vec::new();
+ for cmd in commands.clone() {
+ if command_list[&cmd].borrow().is_hidden() {
+ // unset($commands[array_search($cmd, $commands)])
+ if let Some(idx) = commands.iter().position(|c| *c == cmd) {
+ commands.remove(idx);
+ }
+ formatted_abbrevs.push(PhpMixed::Bool(false));
+ continue;
+ }
+
+ let abbrev = format!(
+ "{} {}",
+ shirabe_php_shim::str_pad(
+ &cmd,
+ max_len as usize,
+ " ",
+ shirabe_php_shim::STR_PAD_RIGHT
+ ),
+ command_list[&cmd].borrow().get_description()
+ );
+
+ if Helper::width(&abbrev) > usable_width {
+ formatted_abbrevs.push(PhpMixed::from(format!(
+ "{}...",
+ Helper::substr(&abbrev, 0, Some(usable_width - 3))
+ )));
+ } else {
+ formatted_abbrevs.push(PhpMixed::from(abbrev));
+ }
+ }
+
+ if commands.len() > 1 {
+ let filtered: Vec<String> = formatted_abbrevs
+ .iter()
+ .filter(|a| shirabe_php_shim::php_truthy(a))
+ .map(|a| shirabe_php_shim::php_to_string(a))
+ .collect();
+ let suggestions = self.get_abbreviation_suggestions(&filtered);
+
+ return Err(CommandNotFoundException::new(
+ format!(
+ "SymfonyCommand \"{}\" is ambiguous.\nDid you mean one of these?\n{}.",
+ PhpMixed::from(name.to_string()),
+ PhpMixed::from(suggestions),
+ ),
+ commands.clone(),
+ 0,
+ )
+ .into());
+ }
+ }
+ }
+
+ // $command = $this->get(reset($commands));
+ let command = self.get(&commands[0])?;
+
+ if command.borrow().is_hidden() {
+ return Err(CommandNotFoundException::new(
+ format!(
+ "The command \"{}\" does not exist.",
+ PhpMixed::from(name.to_string()),
+ ),
+ Vec::new(),
+ 0,
+ )
+ .into());
+ }
+
+ Ok(command)
+ }
+
+ /// Gets the commands (registered in the given namespace if provided).
+ ///
+ /// The array keys are the full names and the values the command instances.
+ pub fn all(
+ &mut self,
+ namespace: Option<&str>,
+ ) -> anyhow::Result<IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
+ self.init()?;
+
+ if namespace.is_none() {
+ if self.command_loader.is_none() {
+ return Ok(self.commands.clone());
+ }
+
+ let mut commands = self.commands.clone();
+ let names = self.command_loader.as_ref().unwrap().get_names();
+ for name in names {
+ if !commands.contains_key(&name) && self.has(&name) {
+ commands.insert(name.clone(), self.get(&name)?);
+ }
+ }
+
+ return Ok(commands);
+ }
+
+ let namespace = namespace.unwrap();
+ let mut commands: IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> =
+ IndexMap::new();
+ let entries: Vec<(String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>)> = self
+ .commands
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect();
+ for (name, command) in entries {
+ if namespace
+ == self.extract_namespace(
+ &name,
+ Some(shirabe_php_shim::substr_count(namespace, ":") + 1),
+ )
+ {
+ commands.insert(name, command);
+ }
+ }
+
+ if self.command_loader.is_some() {
+ let names = self.command_loader.as_ref().unwrap().get_names();
+ for name in names {
+ if !commands.contains_key(&name)
+ && namespace
+ == self.extract_namespace(
+ &name,
+ Some(shirabe_php_shim::substr_count(namespace, ":") + 1),
+ )
+ && self.has(&name)
+ {
+ commands.insert(name.clone(), self.get(&name)?);
+ }
+ }
+ }
+
+ Ok(commands)
+ }
+
+ /// Returns an array of possible abbreviations given a set of names.
+ pub fn get_abbreviations(names: Vec<String>) -> IndexMap<String, Vec<String>> {
+ let mut abbrevs: IndexMap<String, Vec<String>> = IndexMap::new();
+ for name in names {
+ let mut len = shirabe_php_shim::strlen(&name);
+ while len > 0 {
+ let abbrev = shirabe_php_shim::substr(&name, 0, Some(len));
+ abbrevs.entry(abbrev).or_default().push(name.clone());
+ len -= 1;
+ }
+ }
+
+ abbrevs
+ }
+
+ pub fn render_throwable(
+ &self,
+ e: &anyhow::Error,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) {
+ output
+ .borrow()
+ .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET);
+
+ self.do_render_throwable(e, output.clone());
+
+ if let Some(running_command) = &self.running_command {
+ output.borrow().writeln(
+ &[format!(
+ "<info>{}</info>",
+ PhpMixed::from(
+ OutputFormatter::escape(&shirabe_php_shim::sprintf(
+ &running_command.borrow_mut().get_synopsis(false),
+ &[PhpMixed::from(self.get_name())],
+ ))
+ .unwrap(),
+ ),
+ )],
+ output_interface::VERBOSITY_QUIET,
+ );
+ output
+ .borrow()
+ .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET);
+ }
+ }
+
+ pub fn do_render_throwable(
+ &self,
+ e: &anyhow::Error,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) {
+ // do { ... } while ($e = $e->getPrevious());
+ // TODO(review): PHP walks the exception chain via getPrevious() and reads getMessage(),
+ // getCode(), getFile(), getLine(), getTrace(). anyhow::Error exposes a source() chain but
+ // not file/line/trace; faithful rendering of the trace needs a Throwable-equivalent.
+ let _ = output;
+ let _ = e;
+ todo!("render exception chain (getMessage/getCode/getFile/getLine/getTrace/getPrevious)")
+ }
+
+ /// Configures the input and output instances based on the user arguments and options.
+ pub fn configure_io(
+ &self,
+ input: &std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<()> {
+ if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![PhpMixed::from("--ansi".to_string())]),
+ true,
+ ) {
+ output.borrow().set_decorated(true);
+ } else if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![PhpMixed::from("--no-ansi".to_string())]),
+ true,
+ ) {
+ output.borrow().set_decorated(false);
+ }
+
+ if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![
+ PhpMixed::from("--no-interaction".to_string()),
+ PhpMixed::from("-n".to_string()),
+ ]),
+ true,
+ ) {
+ input.borrow_mut().set_interactive(false);
+ }
+
+ let mut shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY").unwrap_or_default();
+ let shell_verbosity_int: i64 = shell_verbosity.parse().unwrap_or(0);
+ let mut shell_verbosity: i64 = shell_verbosity_int;
+ match shell_verbosity_int {
+ -1 => {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_QUIET);
+ }
+ 1 => {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_VERBOSE);
+ }
+ 2 => {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE);
+ }
+ 3 => {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_DEBUG);
+ }
+ _ => {
+ shell_verbosity = 0;
+ }
+ }
+
+ if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![
+ PhpMixed::from("--quiet".to_string()),
+ PhpMixed::from("-q".to_string()),
+ ]),
+ true,
+ ) {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_QUIET);
+ shell_verbosity = -1;
+ } else if input
+ .borrow()
+ .has_parameter_option(PhpMixed::from("-vvv".to_string()), true)
+ || input
+ .borrow()
+ .has_parameter_option(PhpMixed::from("--verbose=3".to_string()), true)
+ || input.borrow().get_parameter_option(
+ PhpMixed::from("--verbose".to_string()),
+ PhpMixed::Bool(false),
+ true,
+ ) == PhpMixed::from(3i64)
+ {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_DEBUG);
+ shell_verbosity = 3;
+ } else if input
+ .borrow()
+ .has_parameter_option(PhpMixed::from("-vv".to_string()), true)
+ || input
+ .borrow()
+ .has_parameter_option(PhpMixed::from("--verbose=2".to_string()), true)
+ || input.borrow().get_parameter_option(
+ PhpMixed::from("--verbose".to_string()),
+ PhpMixed::Bool(false),
+ true,
+ ) == PhpMixed::from(2i64)
+ {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE);
+ shell_verbosity = 2;
+ } else if input
+ .borrow()
+ .has_parameter_option(PhpMixed::from("-v".to_string()), true)
+ || input
+ .borrow()
+ .has_parameter_option(PhpMixed::from("--verbose=1".to_string()), true)
+ || input
+ .borrow()
+ .has_parameter_option(PhpMixed::from("--verbose".to_string()), true)
+ || shirabe_php_shim::php_truthy(&input.borrow().get_parameter_option(
+ PhpMixed::from("--verbose".to_string()),
+ PhpMixed::Bool(false),
+ true,
+ ))
+ {
+ output
+ .borrow()
+ .set_verbosity(output_interface::VERBOSITY_VERBOSE);
+ shell_verbosity = 1;
+ }
+
+ if shell_verbosity == -1 {
+ input.borrow_mut().set_interactive(false);
+ }
+
+ if shirabe_php_shim::function_exists("putenv") {
+ shirabe_php_shim::putenv(&format!("SHELL_VERBOSITY={}", shell_verbosity));
+ }
+ shirabe_php_shim::env_set("SHELL_VERBOSITY", shell_verbosity.to_string());
+ shirabe_php_shim::server_set("SHELL_VERBOSITY", shell_verbosity.to_string());
+
+ let _ = &mut shell_verbosity;
+
+ Ok(())
+ }
+
+ /// Runs the current command.
+ ///
+ /// If an event dispatcher has been attached to the application, events are also
+ /// dispatched during the life-cycle of the command.
+ ///
+ /// Returns 0 if everything went fine, or an error code.
+ pub fn do_run_command(
+ &mut self,
+ command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ if let Some(helper_set) = command.borrow().get_helper_set() {
+ for (_alias, helper) in helper_set.borrow().get_iterator() {
+ // if ($helper instanceof InputAwareInterface) $helper->setInput($input);
+ // TODO(review): downcasting a HelperInterface to InputAwareInterface is not
+ // expressible without a typed mechanism; needs design.
+ let _ = helper;
+ let _ = std::marker::PhantomData::<dyn InputAwareInterface>;
+ }
+ }
+
+ if !self.signals_to_dispatch_event.is_empty() {
+ // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []
+ // TODO(review): SymfonyCommand is not a SignalableCommandInterface here; downcast needed.
+ let command_signals: Vec<i64> = Vec::new();
+ let _ = std::marker::PhantomData::<dyn SignalableCommandInterface>;
+
+ if !command_signals.is_empty() || self.dispatcher.is_some() {
+ if self.signal_registry.is_none() {
+ return Err(ConsoleRuntimeException(shirabe_php_shim::RuntimeException {
+ message: "Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(),
+ code: 0,
+ })
+ .into());
+ }
+
+ if Terminal::has_stty_available() {
+ // TODO: registers SIGINT/SIGTERM handlers that restore the stty mode via
+ // shell_exec('stty ...'). pcntl signal handlers have no faithful Rust
+ // equivalent in Phase A.
+ let _stty_mode = shirabe_php_shim::shell_exec("stty -g");
+ for _signal in [shirabe_php_shim::SIGINT, shirabe_php_shim::SIGTERM] {
+ todo!("register signal handler to restore stty mode");
+ }
+ }
+ }
+
+ if self.dispatcher.is_some() {
+ // TODO(plugin): for each signal, register a handler that dispatches ConsoleSignalEvent.
+ for &signal in &self.signals_to_dispatch_event.clone() {
+ let _event = ConsoleSignalEvent::new(
+ todo!("Box<dyn SymfonyCommand>"),
+ todo!("Box<dyn InputInterface>"),
+ todo!("Box<dyn OutputInterface>"),
+ signal,
+ );
+ todo!("register signal handler dispatching ConsoleEvents::SIGNAL");
+ }
+ }
+
+ for _signal in command_signals {
+ // $this->signalRegistry->register($signal, [$command, 'handleSignal']);
+ todo!("register command->handle_signal as signal handler");
+ }
+ }
+
+ if self.dispatcher.is_none() {
+ return command.borrow_mut().run(
+ &mut *borrow_input_mut(&input),
+ &mut *borrow_output_mut(&output),
+ );
+ }
+
+ // bind before the console.command event, so the listeners have access to input options/arguments
+ match (|| -> anyhow::Result<()> {
+ command.borrow_mut().merge_application_definition(true);
+ input.borrow_mut().bind(command.borrow().get_definition())?;
+ Ok(())
+ })() {
+ Ok(()) => {}
+ Err(e) => {
+ // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
+ if !is_exception_interface(&e) {
+ return Err(e);
+ }
+ }
+ }
+
+ // TODO(plugin): the whole dispatcher block below drives ConsoleCommandEvent /
+ // ConsoleErrorEvent / ConsoleTerminateEvent. The event objects require Box<dyn ...>
+ // wrappers for input/output/command and the dispatcher's dispatch() contract; their
+ // construction is left to the plugin/event design.
+ let _ = ConsoleCommandEvent::RETURN_CODE_DISABLED;
+ let _ = std::marker::PhantomData::<(
+ ConsoleCommandEvent,
+ ConsoleErrorEvent,
+ ConsoleTerminateEvent,
+ )>;
+ todo!("dispatcher-driven command run (console.command / console.error / console.terminate)")
+ }
+
+ /// Gets the name of the command based on input.
+ pub fn get_command_name(&self, input: &dyn InputInterface) -> Option<String> {
+ if self.single_command {
+ Some(self.default_command.clone())
+ } else {
+ input.get_first_argument()
+ }
+ }
+
+ /// Gets the default input definition (Symfony base; `parent::getDefaultInputDefinition`).
+ pub fn base_get_default_input_definition(&self) -> InputDefinition {
+ use shirabe_external_packages::symfony::console::input::input_definition::DefinitionItem;
+ InputDefinition::new(vec![
+ DefinitionItem::InputArgument(
+ InputArgument::new(
+ "command".to_string(),
+ Some(InputArgument::REQUIRED),
+ "The command to execute".to_string(),
+ PhpMixed::Null,
+ )
+ .unwrap(),
+ ),
+ DefinitionItem::InputOption(
+ InputOption::new(
+ "--help",
+ PhpMixed::from("-h".to_string()),
+ Some(InputOption::VALUE_NONE),
+ format!(
+ "Display help for the given command. When no command is given display help for the <info>{}</info> command",
+ self.default_command
+ ),
+ PhpMixed::Null,
+ )
+ .unwrap(),
+ ),
+ DefinitionItem::InputOption(
+ InputOption::new(
+ "--quiet",
+ PhpMixed::from("-q".to_string()),
+ Some(InputOption::VALUE_NONE),
+ "Do not output any message".to_string(),
+ PhpMixed::Null,
+ )
+ .unwrap(),
+ ),
+ DefinitionItem::InputOption(
+ InputOption::new(
+ "--verbose",
+ PhpMixed::from("-v|vv|vvv".to_string()),
+ Some(InputOption::VALUE_NONE),
+ "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug".to_string(),
+ PhpMixed::Null,
+ )
+ .unwrap(),
+ ),
+ DefinitionItem::InputOption(
+ InputOption::new(
+ "--version",
+ PhpMixed::from("-V".to_string()),
+ Some(InputOption::VALUE_NONE),
+ "Display this application version".to_string(),
+ PhpMixed::Null,
+ )
+ .unwrap(),
+ ),
+ DefinitionItem::InputOption(
+ InputOption::new(
+ "--ansi",
+ PhpMixed::from("".to_string()),
+ Some(InputOption::VALUE_NEGATABLE),
+ "Force (or disable --no-ansi) ANSI output".to_string(),
+ PhpMixed::Null,
+ )
+ .unwrap(),
+ ),
+ DefinitionItem::InputOption(
+ InputOption::new(
+ "--no-interaction",
+ PhpMixed::from("-n".to_string()),
+ Some(InputOption::VALUE_NONE),
+ "Do not ask any interactive question".to_string(),
+ PhpMixed::Null,
+ )
+ .unwrap(),
+ ),
+ ])
+ .unwrap()
+ }
+
+ /// Gets the default commands that should always be available (Symfony base;
+ /// `parent::getDefaultCommands`).
+ pub fn base_get_default_commands(
+ &self,
+ ) -> Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
+ // return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
+ // TODO(review): HelpCommand/ListCommand/CompleteCommand/DumpCompletionCommand are ported as
+ // distinct structs (not subtypes of the concrete `SymfonyCommand` struct), so they cannot populate
+ // a Vec<Rc<RefCell<dyn SymfonyCommand>>>. Reconciling SymfonyCommand subclassing with Rust requires the
+ // command-hierarchy design decision (see also `add`/`find`/LazyCommand handling).
+ let _ = std::marker::PhantomData::<(
+ shirabe_external_packages::symfony::console::command::help_command::HelpCommand,
+ shirabe_external_packages::symfony::console::command::list_command::ListCommand,
+ shirabe_external_packages::symfony::console::command::complete_command::CompleteCommand,
+ shirabe_external_packages::symfony::console::command::dump_completion_command::DumpCompletionCommand,
+ )>;
+ todo!("construct default commands once SymfonyCommand-subclass representation is decided")
+ }
+
+ /// Gets the default helper set with the helpers that should always be available.
+ pub fn get_default_helper_set(&self) -> std::rc::Rc<std::cell::RefCell<HelperSet>> {
+ let helper_set = std::rc::Rc::new(std::cell::RefCell::new(HelperSet::default()));
+ let helpers: IndexMap<HelperSetKey, std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>> = {
+ let mut m: IndexMap<
+ HelperSetKey,
+ std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>,
+ > = IndexMap::new();
+ m.insert(
+ HelperSetKey::Int(0),
+ std::rc::Rc::new(std::cell::RefCell::new(FormatterHelper::default())),
+ );
+ m.insert(
+ HelperSetKey::Int(1),
+ std::rc::Rc::new(std::cell::RefCell::new(DebugFormatterHelper::default())),
+ );
+ m.insert(
+ HelperSetKey::Int(2),
+ std::rc::Rc::new(std::cell::RefCell::new(ProcessHelper::default())),
+ );
+ m.insert(
+ HelperSetKey::Int(3),
+ std::rc::Rc::new(std::cell::RefCell::new(QuestionHelper::default())),
+ );
+ m
+ };
+ HelperSet::new(&helper_set, helpers);
+ helper_set
+ }
+
+ /// Returns abbreviated suggestions in string format.
+ fn get_abbreviation_suggestions(&self, abbrevs: &[String]) -> String {
+ format!(" {}", shirabe_php_shim::implode("\n ", abbrevs))
+ }
+
+ /// Returns the namespace part of the command name.
+ ///
+ /// This method is not part of public API and should not be used directly.
+ pub fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String {
+ // $parts = explode(':', $name, -1);
+ let parts = shirabe_php_shim::explode_limit(":", name, -1);
+
+ // implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit))
+ match limit {
+ None => shirabe_php_shim::implode(":", &parts),
+ Some(limit) => {
+ let sliced: Vec<String> = parts.into_iter().take(limit.max(0) as usize).collect();
+ shirabe_php_shim::implode(":", &sliced)
+ }
+ }
+ }
+
+ /// Finds alternative of $name among $collection, if nothing is found in
+ /// $collection, try in $abbrevs.
+ fn find_alternatives(&self, name: &str, collection: &[String]) -> Vec<String> {
+ let threshold = 1e3;
+ let mut alternatives: IndexMap<String, f64> = IndexMap::new();
+
+ let mut collection_parts: IndexMap<String, Vec<String>> = IndexMap::new();
+ for item in collection {
+ collection_parts.insert(item.clone(), shirabe_php_shim::explode(":", item));
+ }
+
+ for (i, subname) in shirabe_php_shim::explode(":", name).into_iter().enumerate() {
+ for (collection_name, parts) in &collection_parts {
+ let exists = alternatives.contains_key(collection_name);
+ if parts.get(i).is_none() && exists {
+ *alternatives.get_mut(collection_name).unwrap() += threshold;
+ continue;
+ } else if parts.get(i).is_none() {
+ continue;
+ }
+
+ let lev = shirabe_php_shim::levenshtein(&subname, &parts[i]) as f64;
+ if lev <= shirabe_php_shim::strlen(&subname) as f64 / 3.0
+ || (!subname.is_empty() && parts[i].contains(&subname))
+ {
+ let v = if exists {
+ alternatives[collection_name] + lev
+ } else {
+ lev
+ };
+ alternatives.insert(collection_name.clone(), v);
+ } else if exists {
+ *alternatives.get_mut(collection_name).unwrap() += threshold;
+ }
+ }
+ }
+
+ for item in collection {
+ let lev = shirabe_php_shim::levenshtein(name, item) as f64;
+ if lev <= shirabe_php_shim::strlen(name) as f64 / 3.0 || item.contains(name) {
+ let v = if alternatives.contains_key(item) {
+ alternatives[item] - lev
+ } else {
+ lev
+ };
+ alternatives.insert(item.clone(), v);
+ }
+ }
+
+ // array_filter($alternatives, fn($lev) => $lev < 2 * $threshold)
+ alternatives.retain(|_, lev| *lev < 2.0 * threshold);
+ // ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE)
+ let mut keys: Vec<String> = alternatives.keys().cloned().collect();
+ shirabe_php_shim::sort_natural_flag_case(&mut keys);
+
+ keys
+ }
+
+ /// Sets the default SymfonyCommand name.
+ pub fn set_default_command(
+ &mut self,
+ command_name: &str,
+ is_single_command: bool,
+ ) -> anyhow::Result<&mut Self> {
+ // $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];
+ let trimmed = shirabe_php_shim::ltrim(command_name, Some("|"));
+ self.default_command = shirabe_php_shim::explode("|", &trimmed)
+ .into_iter()
+ .next()
+ .unwrap_or_default();
+
+ if is_single_command {
+ // Ensure the command exist
+ self.find(command_name)?;
+
+ self.single_command = true;
+ }
+
+ Ok(self)
+ }
+
+ pub fn is_single_command(&self) -> bool {
+ self.single_command
+ }
+
+ fn split_string_by_width(&self, string: &str, width: i64) -> Vec<String> {
+ // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
+ let encoding = match shirabe_php_shim::mb_detect_encoding(string, None, true) {
+ None => return shirabe_php_shim::str_split(string, width),
+ Some(encoding) => encoding,
+ };
+
+ let utf8_string = shirabe_php_shim::mb_convert_encoding(string.into(), "utf8", &encoding);
+ let mut lines: Vec<String> = Vec::new();
+ let mut line = String::new();
+
+ let mut offset = 0i64;
+ let mut m: Vec<String> = Vec::new();
+ while shirabe_php_shim::preg_match_offset(r"/.{1,10000}/u", &utf8_string, &mut m, 0, offset)
+ {
+ offset += shirabe_php_shim::strlen(&m[0]);
+
+ for char in shirabe_php_shim::preg_split_chars(r"//u", &m[0]) {
+ // test if $char could be appended to current line
+ if shirabe_php_shim::mb_strwidth(&format!("{}{}", line, char), Some("utf8"))
+ <= width
+ {
+ line.push_str(&char);
+ continue;
+ }
+ // if not, push current line to array and make new line
+ lines.push(shirabe_php_shim::str_pad(
+ &line,
+ width as usize,
+ " ",
+ shirabe_php_shim::STR_PAD_RIGHT,
+ ));
+ line = char;
+ }
+ }
+
+ lines.push(if !lines.is_empty() {
+ shirabe_php_shim::str_pad(&line, width as usize, " ", shirabe_php_shim::STR_PAD_RIGHT)
+ } else {
+ line.clone()
+ });
+
+ shirabe_php_shim::mb_convert_variables(&encoding, "utf8", &mut lines);
+
+ lines
+ }
+
+ /// Returns all namespaces of the command name.
+ fn extract_all_namespaces(&self, name: &str) -> Vec<String> {
+ // -1 as third argument is needed to skip the command short name when exploding
+ let parts = shirabe_php_shim::explode_limit(":", name, -1);
+ let mut namespaces: Vec<String> = Vec::new();
+
+ for part in parts {
+ if !namespaces.is_empty() {
+ let last = namespaces.last().unwrap().clone();
+ namespaces.push(format!("{}:{}", last, part));
+ } else {
+ namespaces.push(part);
+ }
+ }
+
+ namespaces
+ }
+
+ fn init(&mut self) -> anyhow::Result<()> {
+ if self.initialized {
+ return Ok(());
+ }
+ self.initialized = true;
+
+ for command in self.get_default_commands() {
+ self.add(command)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl BaseApplication for Application {
+ fn get_name(&self) -> String {
+ Application::get_name(self)
+ }
+
+ fn get_version(&self) -> String {
+ Application::get_version(self)
+ }
+
+ fn get_help(&self) -> String {
+ Application::get_help(self)
+ }
+
+ fn is_single_command(&self) -> bool {
+ Application::is_single_command(self)
+ }
+
+ fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String {
+ Application::extract_namespace(self, name, limit)
+ }
+
+ fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String> {
+ Application::find_namespace(self, namespace)
+ }
+
+ fn all(
+ &mut self,
+ namespace: Option<&str>,
+ ) -> anyhow::Result<IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
+ Application::all(self, namespace)
+ }
+
+ fn find(
+ &mut self,
+ name: &str,
+ ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
+ Application::find(self, name)
+ }
+
+ fn get_definition(&mut self) -> std::rc::Rc<std::cell::RefCell<InputDefinition>> {
+ Application::get_definition(self)
+ }
+
+ fn get_helper_set(&mut self) -> std::rc::Rc<std::cell::RefCell<HelperSet>> {
+ Application::get_helper_set(self)
+ }
+
+ fn complete(
+ &mut self,
+ input: &CompletionInput,
+ suggestions: &mut CompletionSuggestions,
+ ) -> anyhow::Result<()> {
+ Application::complete(self, input, suggestions)
+ }
+}
+
+/// Helper mirroring PHP's `$e instanceof ExceptionInterface`.
+fn is_exception_interface(e: &anyhow::Error) -> bool {
+ // anyhow::Error stores concrete error types; enumerate the console exceptions
+ // that implement ExceptionInterface (PHP's `$e instanceof ExceptionInterface`).
+ e.downcast_ref::<CommandNotFoundException>().is_some()
+ || e.downcast_ref::<NamespaceNotFoundException>().is_some()
+ || e.downcast_ref::<ConsoleLogicException>().is_some()
+ || e.downcast_ref::<ConsoleRuntimeException>().is_some()
+}
+
+/// Helper mirroring PHP's `$e instanceof CommandNotFoundException`.
+fn downcast_command_not_found(e: &anyhow::Error) -> Option<&CommandNotFoundException> {
+ if let Some(cnf) = e.downcast_ref::<CommandNotFoundException>() {
+ return Some(cnf);
+ }
+ e.downcast_ref::<NamespaceNotFoundException>().map(|n| &n.0)
+}
+
+/// Helper mirroring PHP's `$e instanceof NamespaceNotFoundException`.
+fn is_namespace_not_found(e: &anyhow::Error) -> bool {
+ e.downcast_ref::<NamespaceNotFoundException>().is_some()
+}
+
+/// Borrows the shared input as a mutable `dyn InputInterface` for passing to
+/// `SymfonyCommand::run`, which takes `&mut dyn InputInterface`.
+fn borrow_input_mut(
+ input: &std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+) -> std::cell::RefMut<'_, dyn InputInterface> {
+ input.borrow_mut()
+}
+
+/// Borrows the shared output as a mutable `dyn OutputInterface` for passing to
+/// `SymfonyCommand::run`, which takes `&mut dyn OutputInterface`.
+fn borrow_output_mut(
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+) -> std::cell::RefMut<'_, dyn OutputInterface> {
+ output.borrow_mut()
+}
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index c54df20..f0ef82f 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -3,9 +3,6 @@
use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::{CaptureKey, Preg};
-use shirabe_external_packages::symfony::console::Application;
-use shirabe_external_packages::symfony::console::input::StringInput;
-use shirabe_external_packages::symfony::console::output::ConsoleOutput;
use shirabe_external_packages::symfony::process::ExecutableFinder;
use shirabe_external_packages::symfony::process::PhpExecutableFinder;
use shirabe_php_shim::{
@@ -573,64 +570,28 @@ impl EventDispatcher {
continue;
}
- let mut app = Application::__construct("UNKNOWN", "UNKNOWN");
- app.set_catch_exceptions(false);
- // PHP: if (method_exists($app, 'setCatchErrors')) { $app->setCatchErrors(false); }
- // This Symfony Console port does not provide `setCatchErrors`, so the
- // guarded call is skipped, matching `method_exists` evaluating to false.
- app.set_auto_exit(false);
- // TODO(plugin): instantiate command class dynamically: `new $className($event->getName())`
+ // PHP hosts the user's Command class in a throwaway, bare
+ // `Symfony\Component\Console\Application` (NOT Composer's Application):
+ // $app = new Application();
+ // $app->setCatchExceptions(false);
+ // $app->setAutoExit(false);
+ // $cmd = new $className($event->getName());
+ // $app->add($cmd);
+ // $app->setDefaultCommand((string) $cmd->getName(), true);
+ // $return = $app->run(new StringInput(...), $output);
+ //
+ // TODO(plugin): a `scripts` entry naming a Symfony Command subclass is run by
+ // hosting it in a bare Symfony console Application. This requires the PHP
+ // runtime — both the dynamic `new $className(...)` instantiation and the real
+ // Symfony Application. It will be implemented by generating a PHP bootstrap
+ // (the boilerplate above) parameterized by the class name, event name and
+ // args, then executing it via the PHP runtime with the child process
+ // inheriting STDOUT/STDERR in place of reusing the in-memory output. No
+ // Rust-side Symfony Application is involved, so none is constructed here.
+ let _ = &additional_args;
todo!(
- "plugin: CommandBase::new — dynamic plugin command instantiation not supported"
+ "plugin: run a `scripts` Command class via the PHP runtime (bare Symfony Application host)"
);
- let result = (|| -> anyhow::Result<i64> {
- let args = additional_args
- .iter()
- .map(|arg| ProcessExecutor::escape(arg))
- .collect::<Vec<_>>()
- .join(" ");
- // reusing the output from $this->io is mostly needed for tests, but generally speaking
- // it does not hurt to keep the same stream as the current Application
- let is_console_io = self.io.borrow().as_any().is::<ConsoleIO>();
- let output: ConsoleOutput = if is_console_io {
- // TODO(plugin): \ReflectionProperty to read private `output` from ConsoleIO
- // is required by the original PHP — needs user-decided porting strategy.
- let _refl_php_version_gate = PHP_VERSION_ID < 80100;
- todo!("\\ReflectionProperty on ConsoleIO::$output")
- } else {
- ConsoleOutput::new(None, None, None)?
- };
- let input_str = event
- .get_flags()
- .get("script-alias-input")
- .and_then(|v| v.as_string())
- .unwrap_or(&args)
- .to_string();
- let input = StringInput::new(&input_str)?;
- let output = output;
- Ok(app.run(
- Some(std::rc::Rc::new(std::cell::RefCell::new(input))),
- Some(std::rc::Rc::new(std::cell::RefCell::new(output))),
- )?)
- })();
- match result {
- Ok(v) => r#return = v,
- Err(e) => {
- self.io.write_error3(
- &format!(
- "<error>{}</error>",
- format!(
- "Script {} handling the {} event terminated with an exception",
- PhpMixed::String(callable_str.clone()),
- PhpMixed::String(event.get_name().to_string()),
- )
- ),
- true,
- crate::io::QUIET,
- );
- return Err(e);
- }
- }
}
Callable::String(callable_str) => {
let args = additional_args
diff --git a/crates/shirabe/src/util/platform.rs b/crates/shirabe/src/util/platform.rs
index ecf2338..5155e8f 100644
--- a/crates/shirabe/src/util/platform.rs
+++ b/crates/shirabe/src/util/platform.rs
@@ -5,8 +5,8 @@ use std::sync::Mutex;
use anyhow::Result;
use shirabe_external_packages::composer::pcre::Preg;
use shirabe_php_shim::{
- PhpMixed, RuntimeException, defined, env_contains_key, env_get, env_set, env_unset,
- file_exists, file_get_contents, fopen, fstat, function_exists, getcwd, getenv, in_array,
+ PhpMixed, PhpResource, RuntimeException, defined, env_contains_key, env_get, env_set,
+ env_unset, file_exists, file_get_contents, fstat, function_exists, getcwd, getenv, in_array,
ini_get, is_array, is_readable, mb_strlen, php_os_family, posix_geteuid, posix_getpwuid,
posix_getuid, posix_isatty, putenv, realpath, server_contains_key, server_get, server_set,
server_unset, stream_isatty, stripos, strlen, strtoupper, substr, usleep,
@@ -288,21 +288,12 @@ impl Platform {
}
/// @param ?resource $fd Open file descriptor or null to default to STDOUT
- pub fn is_tty(fd: Option<PhpMixed>) -> bool {
+ pub fn is_tty(fd: Option<PhpResource>) -> bool {
let fd = match fd {
Some(f) => f,
None => {
- if defined("STDOUT") {
- // TODO(phase-c): map the STDOUT constant to a runtime stdout resource; depends
- // on the unmodeled PHP stream/resource layer.
- todo!("STDOUT constant")
- } else {
- let fd = fopen("php://stdout", "w");
- if matches!(fd, PhpMixed::Bool(false)) {
- return false;
- }
- fd
- }
+ // TODO(phase-c): STDOUT is not yet modeled as a `PhpResource` constant.
+ todo!("STDOUT resource constant")
}
};