//! ref: composer/src/Composer/Command/ExecCommand.php use anyhow::Result; use shirabe_external_packages::symfony::component::console::command::command::Command; use shirabe_external_packages::symfony::component::console::command::command::CommandBase; use shirabe_external_packages::symfony::console::input::input_interface::InputInterface; use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface; use shirabe_php_shim::{PhpMixed, RuntimeException, basename, chdir, getcwd, glob}; use crate::command::base_command::BaseCommand; use crate::composer::Composer; use crate::console::input::input_argument::InputArgument; use crate::console::input::input_option::InputOption; use crate::io::io_interface::IOInterface; #[derive(Debug)] pub struct ExecCommand { inner: CommandBase, composer: Option, io: Option>, } impl ExecCommand { pub fn configure(&mut self) { self.inner .set_name("exec") .set_description("Executes a vendored binary/script") .set_definition(vec![ InputOption::new("list", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_NONE), "", None, vec![]), InputArgument::new( "binary", Some(InputArgument::OPTIONAL), "The binary to run, e.g. phpunit", None, // suggestion callback deferred; binaries listed at runtime via get_binaries vec![], ), InputArgument::new( "args", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Arguments to pass to the binary. Use -- to separate from composer arguments", None, vec![], ), ]) .set_help( "Executes a vendored binary/script.\n\n\ Read more at https://getcomposer.org/doc/03-cli.md#exec" ); } pub fn interact( &self, input: &mut dyn InputInterface, _output: &dyn OutputInterface, ) -> Result<()> { let binaries = self.get_binaries(false)?; if binaries.is_empty() { return Ok(()); } if input.get_argument("binary").as_string_opt().is_some() || input.get_option("list").as_bool().unwrap_or(false) { return Ok(()); } let io = self.inner.get_io(); let binary = io.select( "Binary to run: ".to_string(), binaries.clone(), PhpMixed::String(String::new()), PhpMixed::Int(1), "Invalid binary name \"%s\"".to_string(), false, ); if let Some(idx) = binary.as_int() { input.set_argument("binary", &binaries[idx as usize]); } Ok(()) } pub fn execute( &self, input: &dyn InputInterface, _output: &dyn OutputInterface, ) -> Result { let composer = self.inner.require_composer()?; if input.get_option("list").as_bool().unwrap_or(false) || input.get_argument("binary").as_string_opt().is_none() { let bins = self.get_binaries(true)?; if bins.is_empty() { let bin_dir = composer .get_config() .get("bin-dir") .as_string() .unwrap_or("") .to_string(); return Err(RuntimeException { message: format!( "No binaries found in composer.json or in bin-dir ({})", bin_dir ), code: 0, } .into()); } self.inner .get_io() .write("Available binaries:"); for bin in &bins { self.inner .get_io() .write(&format!("- {}", bin)); } return Ok(0); } let binary = input .get_argument("binary") .as_string() .unwrap_or("") .to_string(); let dispatcher = composer.get_event_dispatcher(); dispatcher.add_listener("__exec_command", &binary); let initial_working_directory = self.inner.get_application().get_initial_working_directory(); if let Some(ref iwd) = initial_working_directory { if getcwd().as_deref() != Some(iwd.as_str()) { chdir(iwd).map_err(|e| RuntimeException { message: format!( "Could not switch back to working directory \"{}\"", iwd.display() ), code: 0, })?; } } let args = input .get_argument("args") .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect::>() }) .unwrap_or_default(); Ok(dispatcher.dispatch_script("__exec_command", true, args)?) } fn get_binaries(&self, for_display: bool) -> Result> { let composer = self.inner.require_composer()?; let bin_dir = composer .get_config() .get("bin-dir") .as_string() .unwrap_or("") .to_string(); let bins = glob(&format!("{}/*", bin_dir)); let local_bins_raw: Vec = composer.get_package().get_binaries(); let local_bins: Vec = if for_display { local_bins_raw .into_iter() .map(|e| format!("{} (local)", e)) .collect() } else { local_bins_raw }; let mut binaries: Vec = Vec::new(); let mut previous_bin: Option = None; for bin in bins.iter().chain(local_bins.iter()) { if let Some(prev) = &previous_bin { if bin == &format!("{}.bat", prev) { continue; } } previous_bin = Some(bin.clone()); binaries.push(basename(bin)); } Ok(binaries) } } impl BaseCommand for ExecCommand { fn inner(&self) -> &CommandBase { &self.inner } fn inner_mut(&mut self) -> &mut CommandBase { &mut self.inner } fn composer(&self) -> Option<&Composer> { self.composer.as_ref() } fn composer_mut(&mut self) -> &mut Option { &mut self.composer } fn io(&self) -> Option<&dyn IOInterface> { self.io.as_deref() } fn io_mut(&mut self) -> &mut Option> { &mut self.io } } impl Command for ExecCommand {}