253 lines
9.2 KiB
Rust
253 lines
9.2 KiB
Rust
use std::{ffi::OsString, io::Write, ops::ControlFlow};
|
|
use uucore::format;
|
|
|
|
use brush_core::{Error, ErrorKind, ExecutionResult, builtins, escape, expansion};
|
|
|
|
/// Format a string.
|
|
#[derive(winnow_args::Args)]
|
|
#[arg(disable_help_flag, disable_version_flag, disable_help_subcommand)]
|
|
pub(crate) struct PrintfCommand {
|
|
/// If specified, the output of the command is assigned to this variable.
|
|
#[arg(short = 'v', allow_hyphen_values)]
|
|
output_variables: Vec<String>,
|
|
|
|
/// Format string + arguments to the format string.
|
|
#[arg(positional, stop_flags, required)]
|
|
format_and_args: Vec<String>,
|
|
}
|
|
|
|
brush_winnow_args_adapter::impl_parse_args!(PrintfCommand);
|
|
|
|
impl<SE: brush_core::ShellExtensions> builtins::Builtin<SE> for PrintfCommand {
|
|
type Error = brush_core::Error;
|
|
|
|
async fn execute(
|
|
self,
|
|
context: brush_core::ExecutionContext<'_, SE>,
|
|
) -> Result<ExecutionResult, Self::Error> {
|
|
// Every -v name must be valid, not just the last one, which is used.
|
|
let parser_options = context.shell.parser_options();
|
|
if let Some(invalid) = self
|
|
.output_variables
|
|
.iter()
|
|
.find(|name| !valid_output_variable(name, &parser_options))
|
|
{
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: `{invalid}': not a valid identifier",
|
|
context.command_name
|
|
)?;
|
|
return Ok(ExecutionResult::new(2));
|
|
}
|
|
|
|
if let Some(variable_name) = self.output_variables.last() {
|
|
// Format to a u8 vector.
|
|
let mut result: Vec<u8> = vec![];
|
|
format(self.format_and_args.as_slice(), &mut result)?;
|
|
|
|
// Convert to a string.
|
|
let result_str = String::from_utf8(result).map_err(|_| {
|
|
brush_core::ErrorKind::PrintfInvalidUsage("invalid UTF-8 output".into())
|
|
})?;
|
|
|
|
// Assign to the selected variable.
|
|
expansion::assign_to_named_parameter(
|
|
context.shell,
|
|
&context.params,
|
|
variable_name,
|
|
result_str,
|
|
)
|
|
.await?;
|
|
} else {
|
|
format(self.format_and_args.as_slice(), context.stdout())?;
|
|
context.stdout().flush()?;
|
|
}
|
|
|
|
Ok(ExecutionResult::success())
|
|
}
|
|
}
|
|
|
|
/// Whether `name` is a variable name, or one followed by a non-empty `[subscript]`,
|
|
/// by the grammar of an assignment's name. The subscript isn't evaluated here.
|
|
fn valid_output_variable(name: &str, options: &brush_parser::ParserOptions) -> bool {
|
|
use brush_parser::ast::{AssignmentName, AssignmentValue};
|
|
match brush_parser::word::parse_scalar_assignment(&format!("{name}="), options) {
|
|
Ok(assignment) if !assignment.append => {
|
|
matches!(&assignment.value, AssignmentValue::Scalar(word) if word.value.is_empty())
|
|
&& match assignment.name {
|
|
AssignmentName::VariableName(_) => true,
|
|
AssignmentName::ArrayElementName(_, index) => !index.is_empty(),
|
|
}
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn format(format_and_args: &[String], writer: impl Write) -> Result<(), brush_core::Error> {
|
|
match format_and_args {
|
|
// Handle format string with arguments using uucore
|
|
[fmt, args @ ..] => format_via_uucore(fmt, args.iter(), writer),
|
|
// Handle case with no format string (we shouldn't be able to get here since parsing
|
|
// fails when the format string is missing)
|
|
[] => Err(ErrorKind::PrintfInvalidUsage("missing operand".into()).into()),
|
|
}
|
|
}
|
|
|
|
fn format_via_uucore(
|
|
format_string: &str,
|
|
args: impl Iterator<Item = impl Into<OsString>>,
|
|
mut writer: impl Write,
|
|
) -> Result<(), brush_core::Error> {
|
|
// Convert string arguments to FormatArgument::Unparsed
|
|
let format_args: Vec<_> = args
|
|
.map(|s| format::FormatArgument::Unparsed(s.into()))
|
|
.collect();
|
|
|
|
// Parse format string once.
|
|
let format_items = parse_format_string(format_string)?;
|
|
|
|
// Wrap the format arguments.
|
|
let mut format_args_wrapper = format::FormatArguments::new(&format_args);
|
|
|
|
// Determine whether the format string contains any specifiers that consume arguments. If it
|
|
// doesn't, then we must only run through it once -- even when extra arguments are provided --
|
|
// since otherwise we'd loop forever waiting for arguments that will never be consumed. This
|
|
// matches the behavior of other shells, which print such a format string exactly once.
|
|
let format_consumes_args = format_items
|
|
.iter()
|
|
.any(|(item, _)| matches!(item, format::FormatItem::Spec(_)));
|
|
|
|
// Keep going until we've exhausted all format arguments. Also make sure to run at least once
|
|
// even if there's no format arguments.
|
|
while format_args.is_empty() || !format_args_wrapper.is_exhausted() {
|
|
// Process all format items, in order. We'll bail when we're told to stop.
|
|
for (item, backslash_quote) in &format_items {
|
|
if let (format::FormatItem::Spec(format::Spec::QuotedString { position }), true) =
|
|
(item, *backslash_quote)
|
|
{
|
|
let arg = format_args_wrapper.next_string(*position).to_string_lossy();
|
|
let quoted = quote_printf_q(&arg);
|
|
write!(writer, "{quoted}")?;
|
|
continue;
|
|
}
|
|
|
|
let control_flow = item
|
|
.write(&mut writer, &mut format_args_wrapper)
|
|
.map_err(|e| match e {
|
|
// Propagate I/O errors directly so they can be handled appropriately
|
|
format::FormatError::IoError(io_err) => Error::from(io_err),
|
|
// Wrap other format errors
|
|
other => Error::from(ErrorKind::PrintfInvalidUsage(std::format!(
|
|
"printf formatting error: {other}"
|
|
))),
|
|
})?;
|
|
|
|
if control_flow == ControlFlow::Break(()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the format string doesn't consume any arguments, stop now; otherwise we'd reprocess
|
|
// it forever since no arguments will ever be consumed.
|
|
if !format_consumes_args {
|
|
break;
|
|
}
|
|
|
|
// Start next batch if not exhausted
|
|
if !format_args_wrapper.is_exhausted() {
|
|
format_args_wrapper.start_next_batch();
|
|
}
|
|
|
|
if format_args.is_empty() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn quote_printf_q(s: &str) -> String {
|
|
// Like bash's, this escapes commas too, so the result is safe even inside a brace
|
|
// expansion.
|
|
let options = escape::QuoteOptions::builder()
|
|
.preferred_mode(escape::QuoteMode::BackslashEscape)
|
|
.quote_commas(true)
|
|
.build();
|
|
escape::quote(s, &options).into_owned()
|
|
}
|
|
|
|
type ParsedFormatItem = (format::FormatItem<format::EscapedChar>, bool);
|
|
|
|
fn parse_format_string(format_string: &str) -> Result<Vec<ParsedFormatItem>, brush_core::Error> {
|
|
let format_items: Result<Vec<_>, _> = format::parse_spec_and_escape(format_string.as_bytes())
|
|
.map(|result| match result {
|
|
Ok(item @ format::FormatItem::Spec(format::Spec::QuotedString { .. })) => {
|
|
Ok((item, true))
|
|
}
|
|
Ok(item) => Ok((item, false)),
|
|
// Fixed q/Q modifiers are deliberately ignored; dynamic modifiers remain unsupported
|
|
// until uucore exposes quoted-string metadata.
|
|
Err(format::FormatError::SpecError(spec, span))
|
|
if matches!(spec.last(), Some(b'q' | b'Q'))
|
|
&& !spec.contains(&b'*')
|
|
&& !spec.contains(&b'$') =>
|
|
{
|
|
let mut bare_q: &[u8] = b"q";
|
|
let item = format::Spec::parse(&mut bare_q)
|
|
.map(format::FormatItem::Spec)
|
|
.map_err(|spec| format::FormatError::SpecError(spec.to_vec(), span))?;
|
|
Ok((item, !spec.contains(&b'#')))
|
|
}
|
|
Err(error) => Err(error),
|
|
})
|
|
.collect();
|
|
|
|
// Observe any errors we encountered along the way.
|
|
let format_items = format_items
|
|
.map_err(|e| ErrorKind::PrintfInvalidUsage(format!("printf parsing error: {e}")))?;
|
|
|
|
Ok(format_items)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[expect(clippy::panic_in_result_fn)]
|
|
mod tests {
|
|
use super::*;
|
|
use anyhow::Result;
|
|
|
|
fn sprintf_via_uucore(
|
|
format_string: &str,
|
|
args: impl Iterator<Item = impl Into<OsString>>,
|
|
) -> Result<String> {
|
|
let mut result = vec![];
|
|
format_via_uucore(format_string, args, &mut result)?;
|
|
|
|
Ok(String::from_utf8(result)?)
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_sprintf() -> Result<()> {
|
|
assert_eq!(sprintf_via_uucore("%s", std::iter::once(&"xyz"))?, "xyz");
|
|
assert_eq!(sprintf_via_uucore(r"%d\n", std::iter::once(&"1"))?, "1\n");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_sprintf_without_args() -> Result<()> {
|
|
let empty: [&str; 0] = [];
|
|
|
|
assert_eq!(sprintf_via_uucore("xyz", empty.iter())?, "xyz");
|
|
assert_eq!(sprintf_via_uucore("%s|", empty.iter())?, "|");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_sprintf_with_cycles() -> Result<()> {
|
|
assert_eq!(sprintf_via_uucore("%s|", ["x", "y"].iter())?, "x|y|");
|
|
|
|
Ok(())
|
|
}
|
|
}
|