//! ref: composer/src/Composer/Command/ExecCommand.php use crate::command::BaseCommand; use crate::command::BaseCommandData; use crate::command::base_command::base_command_initialize; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::io::IOInterfaceImmutable; use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{PhpMixed, RuntimeException, basename, chdir, getcwd, glob}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] pub struct ExecCommand { base_command_data: BaseCommandData, } impl Default for ExecCommand { fn default() -> Self { Self::new() } } impl ExecCommand { pub fn new() -> Self { let command = ExecCommand { base_command_data: BaseCommandData::new(None), }; command .configure() .expect("ExecCommand::configure uses static, valid metadata"); command } fn get_binaries(&self, for_display: bool) -> anyhow::Result> { let composer = self.require_composer(None, None)?; let composer_ref = crate::composer::composer_full(&composer); let bin_dir = composer_ref .get_config() .borrow_mut() .get("bin-dir") .as_string() .unwrap_or("") .to_string(); let bins = glob(&format!("{}/*", bin_dir)); let local_bins_raw: Vec = composer_ref.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 && bin == &format!("{}.bat", prev) { continue; } previous_bin = Some(bin.clone()); binaries.push(basename(bin)); } Ok(binaries) } } impl Command for ExecCommand { fn configure(&self) -> anyhow::Result<()> { self.set_name("exec")?; self.set_description("Executes a vendored binary/script"); self.set_definition(&[ InputOption::new("list", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_NONE), "", None).unwrap().into(), // 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).unwrap().into(), InputArgument::new("args", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Arguments to pass to the binary. Use -- to separate from composer arguments", None).unwrap().into(), ]); self.set_help( "Executes a vendored binary/script.\n\n\ Read more at https://getcomposer.org/doc/03-cli.md#exec", ); Ok(()) } fn interact( &self, input: Rc>, _output: Rc>, ) { let _ = (|| -> anyhow::Result<()> { let binaries = self.get_binaries(false)?; if binaries.is_empty() { return Ok(()); } if input.borrow().get_argument("binary")?.as_string().is_some() || input .borrow() .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.borrow_mut().set_argument( "binary", shirabe_php_shim::PhpMixed::String(binaries[idx as usize].clone()), ); } Ok(()) })(); } fn execute( &self, input: Rc>, _output: Rc>, ) -> anyhow::Result { let composer = self.require_composer(None, None)?; if input .borrow() .get_option("list")? .as_bool() .unwrap_or(false) || input.borrow().get_argument("binary")?.as_string().is_none() { let bins = self.get_binaries(true)?; if bins.is_empty() { let bin_dir = crate::composer::composer_full(&composer) .get_config() .borrow_mut() .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 .borrow() .get_argument("binary")? .as_string() .unwrap_or("") .to_string(); let dispatcher = crate::composer::composer_full(&composer) .get_event_dispatcher() .clone(); dispatcher.borrow_mut().add_listener( "__exec_command", crate::event_dispatcher::Callable::String(binary), 0, ); // TODO(phase-c): getApplication()->getInitialWorkingDirectory() needs the shared shirabe // Application handle (deferred with the Application shared-ownership work). Until then the // working-directory restore is skipped. let initial_working_directory: Option = None; if let Some(ref iwd) = initial_working_directory && getcwd().as_deref() != Some(iwd.as_str()) { chdir(iwd).map_err(|e| RuntimeException { message: format!("Could not switch back to working directory \"{}\"", iwd), code: 0, })?; } let args = input .borrow() .get_argument("args")? .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect::>() }) .unwrap_or_default(); dispatcher.borrow_mut().dispatch_script( "__exec_command", true, args, indexmap::IndexMap::new(), ) } fn initialize( &self, input: Rc>, output: Rc>, ) -> anyhow::Result<()> { base_command_initialize(self, input, output) } shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } impl BaseCommand for ExecCommand { fn command_data( &self, ) -> &shirabe_external_packages::symfony::console::command::command::CommandData { self.base_command_data.command_data() } crate::delegate_base_command_trait_impls_to_inner!(base_command_data); }