1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

printf: use non-zero indexes

This commit is contained in:
Justin Tracey 2025-04-28 13:21:48 -04:00
parent e92e419a93
commit 27487be267
No known key found for this signature in database
GPG key ID: 62B84F5ABDDDCE54
3 changed files with 49 additions and 33 deletions

View file

@ -12,7 +12,7 @@ use crate::{
show_error, show_warning,
};
use os_display::Quotable;
use std::ffi::OsStr;
use std::{ffi::OsStr, num::NonZero};
/// An argument for formatting
///
@ -129,8 +129,9 @@ impl<'a> FormatArguments<'a> {
}
}
fn get_at_relative_position(&mut self, pos: usize) -> Option<&'a FormatArgument> {
let pos = pos.saturating_sub(1).saturating_add(self.current_offset);
fn get_at_relative_position(&mut self, pos: NonZero<usize>) -> Option<&'a FormatArgument> {
let pos: usize = pos.into();
let pos = (pos - 1).saturating_add(self.current_offset);
self.highest_arg_position = Some(self.highest_arg_position.map_or(pos, |x| x.max(pos)));
self.args.get(pos)
}
@ -281,6 +282,10 @@ mod tests {
assert!(args.is_exhausted());
}
fn non_zero_pos(n: usize) -> ArgumentLocation {
ArgumentLocation::Position(NonZero::new(n).unwrap())
}
#[test]
fn test_position_access_pattern() {
// Test with consistent positional access patterns
@ -297,23 +302,23 @@ mod tests {
]);
// First batch - positional access
assert_eq!(b'b', args.next_char(&ArgumentLocation::Position(2))); // Position 2
assert_eq!(b'a', args.next_char(&ArgumentLocation::Position(1))); // Position 1
assert_eq!(b'c', args.next_char(&ArgumentLocation::Position(3))); // Position 3
assert_eq!(b'b', args.next_char(&non_zero_pos(2))); // Position 2
assert_eq!(b'a', args.next_char(&non_zero_pos(1))); // Position 1
assert_eq!(b'c', args.next_char(&non_zero_pos(3))); // Position 3
args.start_next_batch();
assert!(!args.is_exhausted());
// Second batch - same positional pattern
assert_eq!(b'e', args.next_char(&ArgumentLocation::Position(2))); // Position 2
assert_eq!(b'd', args.next_char(&ArgumentLocation::Position(1))); // Position 1
assert_eq!(b'f', args.next_char(&ArgumentLocation::Position(3))); // Position 3
assert_eq!(b'e', args.next_char(&non_zero_pos(2))); // Position 2
assert_eq!(b'd', args.next_char(&non_zero_pos(1))); // Position 1
assert_eq!(b'f', args.next_char(&non_zero_pos(3))); // Position 3
args.start_next_batch();
assert!(!args.is_exhausted());
// Third batch - same positional pattern (last batch)
assert_eq!(b'h', args.next_char(&ArgumentLocation::Position(2))); // Position 2
assert_eq!(b'g', args.next_char(&ArgumentLocation::Position(1))); // Position 1
assert_eq!(b'i', args.next_char(&ArgumentLocation::Position(3))); // Position 3
assert_eq!(b'h', args.next_char(&non_zero_pos(2))); // Position 2
assert_eq!(b'g', args.next_char(&non_zero_pos(1))); // Position 1
assert_eq!(b'i', args.next_char(&non_zero_pos(3))); // Position 3
args.start_next_batch();
assert!(args.is_exhausted());
}
@ -334,19 +339,19 @@ mod tests {
// First batch - mix of sequential and positional
assert_eq!(b'a', args.next_char(&ArgumentLocation::NextArgument)); // Sequential
assert_eq!(b'c', args.next_char(&ArgumentLocation::Position(3))); // Positional
assert_eq!(b'c', args.next_char(&non_zero_pos(3))); // Positional
args.start_next_batch();
assert!(!args.is_exhausted());
// Second batch - same mixed pattern
assert_eq!(b'd', args.next_char(&ArgumentLocation::NextArgument)); // Sequential
assert_eq!(b'f', args.next_char(&ArgumentLocation::Position(3))); // Positional
assert_eq!(b'f', args.next_char(&non_zero_pos(3))); // Positional
args.start_next_batch();
assert!(!args.is_exhausted());
// Last batch - same mixed pattern
assert_eq!(b'g', args.next_char(&ArgumentLocation::NextArgument)); // Sequential
assert_eq!(b'\0', args.next_char(&ArgumentLocation::Position(3))); // Out of bounds
assert_eq!(b'\0', args.next_char(&non_zero_pos(3))); // Out of bounds
args.start_next_batch();
assert!(args.is_exhausted());
}
@ -419,16 +424,16 @@ mod tests {
let mut args = FormatArguments::new(&args);
// First batch - positional access of different types
assert_eq!(b'a', args.next_char(&ArgumentLocation::Position(1)));
assert_eq!("test", args.next_string(&ArgumentLocation::Position(2)));
assert_eq!(42, args.next_u64(&ArgumentLocation::Position(3)));
assert_eq!(b'a', args.next_char(&non_zero_pos(1)));
assert_eq!("test", args.next_string(&non_zero_pos(2)));
assert_eq!(42, args.next_u64(&non_zero_pos(3)));
args.start_next_batch();
assert!(!args.is_exhausted());
// Second batch - same pattern
assert_eq!(b'b', args.next_char(&ArgumentLocation::Position(1)));
assert_eq!("more", args.next_string(&ArgumentLocation::Position(2)));
assert_eq!(99, args.next_u64(&ArgumentLocation::Position(3)));
assert_eq!(b'b', args.next_char(&non_zero_pos(1)));
assert_eq!("more", args.next_string(&non_zero_pos(2)));
assert_eq!(99, args.next_u64(&non_zero_pos(3)));
args.start_next_batch();
assert!(args.is_exhausted());
}
@ -446,13 +451,13 @@ mod tests {
// First batch
assert_eq!(b'a', args.next_char(&ArgumentLocation::NextArgument));
assert_eq!(b'c', args.next_char(&ArgumentLocation::Position(3)));
assert_eq!(b'c', args.next_char(&non_zero_pos(3)));
args.start_next_batch();
assert!(!args.is_exhausted());
// Second batch (partial)
assert_eq!(b'd', args.next_char(&ArgumentLocation::NextArgument));
assert_eq!(b'\0', args.next_char(&ArgumentLocation::Position(3))); // Out of bounds
assert_eq!(b'\0', args.next_char(&non_zero_pos(3))); // Out of bounds
args.start_next_batch();
assert!(args.is_exhausted());
}

View file

@ -16,7 +16,7 @@ use super::{
parse_escape_only,
};
use crate::format::FormatArguments;
use std::{io::Write, ops::ControlFlow};
use std::{io::Write, num::NonZero, ops::ControlFlow};
/// A parsed specification for formatting a value
///
@ -70,7 +70,7 @@ pub enum Spec {
#[derive(Clone, Copy, Debug)]
pub enum ArgumentLocation {
NextArgument,
Position(usize),
Position(NonZero<usize>),
}
/// Precision and width specified might use an asterisk to indicate that they are
@ -156,7 +156,9 @@ impl Spec {
let start = *rest;
// Check for a positional specifier (%m$)
let position = eat_argument_position(rest, &mut index);
let Some(position) = eat_argument_position(rest, &mut index) else {
return Err(&start[..index]);
};
let flags = Flags::parse(rest, &mut index);
@ -566,19 +568,19 @@ fn write_padded(
}
// Check for a number ending with a '$'
fn eat_argument_position(rest: &mut &[u8], index: &mut usize) -> ArgumentLocation {
fn eat_argument_position(rest: &mut &[u8], index: &mut usize) -> Option<ArgumentLocation> {
let original_index = *index;
if let Some(pos) = eat_number(rest, index) {
if let Some(&b'$') = rest.get(*index) {
*index += 1;
ArgumentLocation::Position(pos)
Some(ArgumentLocation::Position(NonZero::new(pos)?))
} else {
*index = original_index;
ArgumentLocation::NextArgument
Some(ArgumentLocation::NextArgument)
}
} else {
*index = original_index;
ArgumentLocation::NextArgument
Some(ArgumentLocation::NextArgument)
}
}
@ -586,7 +588,7 @@ fn eat_asterisk_or_number(rest: &mut &[u8], index: &mut usize) -> Option<CanAste
if let Some(b'*') = rest.get(*index) {
*index += 1;
// Check for a positional specifier (*m$)
Some(CanAsterisk::Asterisk(eat_argument_position(rest, index)))
Some(CanAsterisk::Asterisk(eat_argument_position(rest, index)?))
} else {
eat_number(rest, index).map(CanAsterisk::Fixed)
}
@ -670,7 +672,9 @@ mod tests {
assert_eq!(
Some((2, false)),
resolve_asterisk_width(
Some(CanAsterisk::Asterisk(ArgumentLocation::Position(2))),
Some(CanAsterisk::Asterisk(ArgumentLocation::Position(
NonZero::new(2).unwrap()
))),
&mut FormatArguments::new(&[
FormatArgument::Unparsed("1".to_string()),
FormatArgument::Unparsed("2".to_string()),
@ -738,7 +742,9 @@ mod tests {
assert_eq!(
Some(2),
resolve_asterisk_precision(
Some(CanAsterisk::Asterisk(ArgumentLocation::Position(2))),
Some(CanAsterisk::Asterisk(ArgumentLocation::Position(
NonZero::new(2).unwrap()
))),
&mut FormatArguments::new(&[
FormatArgument::Unparsed("1".to_string()),
FormatArgument::Unparsed("2".to_string()),

View file

@ -1341,6 +1341,11 @@ fn positional_format_specifiers() {
.succeeds()
.stdout_only("05-");
new_ucmd!()
.args(&["%0$d%d-", "5", "10", "6", "20"])
.fails_with_code(1)
.stderr_only("printf: %0$: invalid conversion specification\n");
new_ucmd!()
.args(&[
"Octal: %6$o, Int: %1$d, Float: %4$f, String: %2$s, Hex: %7$x, Scientific: %5$e, Char: %9$c, Unsigned: %3$u, Integer: %8$i",