06aa0c8f创建于 24 天前历史提交
use std::io::Write;

use brush_core::{ExecutionResult, builtins, error, history};
use clap::Parser;

/// Process command history list.
#[derive(Parser)]
pub(crate) struct FcCommand {
	/// List commands instead of editing them.
	#[arg(short = 'l')]
	list: bool,

	/// Suppress line numbers when listing.
	#[arg(short = 'n', requires = "list")]
	no_line_numbers: bool,

	/// Reverse the order of commands.
	#[arg(short = 'r')]
	reverse: bool,

	/// Re-execute command after substitution (old=new format).
	#[arg(short = 's')]
	substitute: bool,

	/// Editor to use (only relevant when not listing or substituting).
	#[arg(short = 'e', value_name = "ENAME")]
	editor: Option<String>,

	/// First command in range (number or string prefix).
	#[arg(value_name = "FIRST", allow_hyphen_values = true)]
	first: Option<String>,

	/// Last command in range (number or string prefix).
	#[arg(value_name = "LAST", allow_hyphen_values = true)]
	last: Option<String>,
}

impl builtins::Command for FcCommand {
	type Error = brush_core::Error;

	async fn execute<SE: brush_core::ShellExtensions>(
		&self,
		context: brush_core::ExecutionContext<'_, SE>,
	) -> Result<ExecutionResult, Self::Error> {
		if self.substitute {
			return self.do_execute(context).await;
		}

		if self.list {
			return self.do_list(&context);
		}

		error::unimp("fc editor mode is not yet implemented")
	}
}

impl FcCommand {
	fn do_list(
		&self,
		context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
	) -> Result<ExecutionResult, brush_core::Error> {
		let history = context
			.shell
			.history()
			.ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?;

		let (first_idx, last_idx, reverse) = self.resolve_range(history)?;

		// Determine the order of iteration
		let indices: Vec<usize> = if reverse {
			(first_idx..=last_idx).rev().collect()
		} else {
			(first_idx..=last_idx).collect()
		};

		for idx in indices {
			if let Some(item) = history.get(idx) {
				if self.no_line_numbers {
					// With -n, bash still outputs a tab before the command
					writeln!(context.stdout(), "\t {}", item.command_line)?;
				} else {
					// Match bash's fc format: number, tab, command
					writeln!(context.stdout(), "{}\t {}", idx + 1, item.command_line)?;
				}
			}
		}

		Ok(ExecutionResult::success())
	}

	async fn do_execute(
		&self,
		context: brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
	) -> Result<ExecutionResult, brush_core::Error> {
		let history = context
			.shell
			.history()
			.ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?;

		// Parse the first argument for pattern=replacement
		let (pattern, replacement) = self
			.first
			.as_ref()
			.and_then(|s| s.split_once('='))
			.map_or((None, None), |(p, r)| (Some(p), Some(r)));

		// Determine which command to re-execute
		let cmd_spec = if pattern.is_some() {
			// If we have a pattern, the command spec is in 'last' if present
			self.last.as_deref()
		} else {
			// Otherwise, it's in 'first'
			self.first.as_deref()
		};

		// Find the command
		let cmd_line = if let Some(spec) = cmd_spec {
			Self::find_command_by_specifier(history, spec)?
		} else {
			// No spec means use the previous command (excluding the fc command itself)
			let effective_count = effective_history_count(history);
			history
				.get(effective_count.saturating_sub(1))
				.map(|item| item.command_line.clone())
				.ok_or_else(|| brush_core::Error::from(error::ErrorKind::HistoryItemNotFound))?
		};

		// Apply substitution if present
		let final_cmd = if let (Some(pat), Some(rep)) = (pattern, replacement) {
			cmd_line.replace(pat, rep)
		} else {
			cmd_line
		};

		// Echo the command to stderr.
		writeln!(context.stderr(), "{final_cmd}")?;

		// Remove the fc command from history before executing the substituted command
		// This matches bash behavior where the fc command is replaced by the executed
		// command
		let history_mut = context
			.shell
			.history_mut()
			.ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?;
		history_mut.remove_nth_item(history_mut.count().saturating_sub(1));

		let source_info = brush_core::SourceInfo::from("(history)");

		// Execute the command
		let result = context
			.shell
			.run_string(final_cmd.clone(), &source_info, &context.params)
			.await?;

		// Add the executed command to history.
		context.shell.add_to_history(&final_cmd)?;

		Ok(result)
	}

	fn resolve_range(
		&self,
		history: &history::History,
	) -> Result<(usize, usize, bool), brush_core::Error> {
		let effective_count = effective_history_count(history);
		let max_idx = effective_count.saturating_sub(1);

		// Resolve first index
		let first_idx = self
			.first
			.as_ref()
			.map(|s| Self::resolve_position(history, s))
			.transpose()?
			.unwrap_or_else(|| {
				if self.list {
					effective_count.saturating_sub(16) // Default for listing: -16
				} else {
					max_idx // Default for editing: previous command
				}
			});

		// Resolve last index (default depends on mode and first_idx)
		let default_last = if self.list { max_idx } else { first_idx };
		let last_idx = self
			.last
			.as_ref()
			.map(|s| Self::resolve_position(history, s))
			.transpose()?
			.unwrap_or(default_last);

		// If first > last, swap them and indicate reversal
		let (first_idx, last_idx, force_reverse) = if first_idx > last_idx {
			(last_idx, first_idx, true)
		} else {
			(first_idx, last_idx, false)
		};

		// Clamp both indices to valid range
		Ok((first_idx.min(max_idx), last_idx.min(max_idx), force_reverse || self.reverse))
	}

	/// Resolves a position specifier (number or string prefix) to a history
	/// index. NOTE: The returned index may still be out of range if the history
	/// is empty.
	///
	/// # Arguments
	///
	/// * `history` - The history to resolve against.
	/// * `spec` - The position specifier (number or string prefix).
	fn resolve_position(history: &history::History, spec: &str) -> Result<usize, brush_core::Error> {
		// Try to parse it as a number. If it's not parseable, then we need to assume
		// it's a string prefix we need to search for.
		let Ok(num) = spec.parse::<i64>() else {
			// Not a number, treat as string prefix
			return Self::find_command_by_prefix(history, spec);
		};

		let effective_count = effective_history_count(history);

		#[expect(clippy::cast_sign_loss)]
		#[expect(clippy::cast_possible_truncation)]
		let result = match num.cmp(&0) {
			std::cmp::Ordering::Equal => {
				// 0 means -1 for listing (relative to effective count)
				effective_count.saturating_sub(1)
			},
			std::cmp::Ordering::Greater => {
				// Positive: 1-based index
				let idx = (num - 1) as usize;
				if idx < effective_count {
					idx
				} else {
					// Out of range - use 0 (first item)
					0
				}
			},
			std::cmp::Ordering::Less => {
				// Negative: offset from end (relative to effective count)
				let offset = (-num) as usize;
				effective_count.saturating_sub(offset)
			},
		};

		Ok(result)
	}

	/// Finds the command matching the given specifier (number or string prefix).
	/// Returns the command line. Returns an error if no such command can be
	/// found in the history.
	///
	/// # Arguments
	///
	/// * `history` - The history to search.
	/// * `spec` - The position spec
	fn find_command_by_specifier(
		history: &history::History,
		spec: &str,
	) -> Result<String, brush_core::Error> {
		let idx = Self::resolve_position(history, spec)?;
		history
			.get(idx)
			.map(|item| item.command_line.clone())
			.ok_or_else(|| brush_core::Error::from(error::ErrorKind::HistoryItemNotFound))
	}

	/// Finds the most recent command starting with the given prefix. Returns
	/// the index of the command in the history. Returns an error if no such
	/// command can be found in the history.
	///
	/// # Arguments
	///
	/// * `history` - The history to search.
	/// * `prefix` - The command prefix to search for.
	fn find_command_by_prefix(
		history: &history::History,
		prefix: &str,
	) -> Result<usize, brush_core::Error> {
		// Search backwards for a command starting with the prefix (excluding fc command
		// itself)
		let effective_count = effective_history_count(history);

		for idx in (0..effective_count).rev() {
			if let Some(item) = history.get(idx) {
				if item.command_line.starts_with(prefix) {
					return Ok(idx);
				}
			}
		}

		Err(brush_core::Error::from(error::ErrorKind::HistoryItemNotFound))
	}
}

/// Returns the effective history count (excluding the fc command itself).
fn effective_history_count(history: &history::History) -> usize {
	history.count().saturating_sub(1)
}