//! ref: composer/src/Composer/Command/ExecCommand.php use anyhow::Result; 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, BaseCommandData, HasBaseCommandData}; 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 { base_command_data: BaseCommandData, } impl ExecCommand { pub fn configure(&mut self) { self .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), // TODO(cli-completion): suggest installed binary names (via get_binaries) for `binary` argument InputArgument::new( "binary", Some(InputArgument::OPTIONAL), "The binary to run, e.g. phpunit", None, ), InputArgument::new( "args", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Arguments to pass to the binary. Use -- to separate from composer arguments", None, ), ]) .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.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", shirabe_php_shim::PhpMixed::String(binaries[idx as usize].clone()), ); } Ok(()) } pub fn execute( &self, input: &dyn InputInterface, _output: &dyn OutputInterface, ) -> Result { let composer = self.require_composer(None, None)?; 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.get_io() .write("Available binaries:"); for bin in &bins { self.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(); // TODO(phase-b): add_listener takes a Callable; wiring binary as callable not yet ported let _ = (dispatcher, &binary); let initial_working_directory = self.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.require_composer(None, None)?; 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 HasBaseCommandData for ExecCommand { fn base_command_data(&self) -> &BaseCommandData { &self.base_command_data } fn base_command_data_mut(&mut self) -> &mut BaseCommandData { &mut self.base_command_data } }