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

View file

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

View file

@ -1341,6 +1341,11 @@ fn positional_format_specifiers() {
.succeeds() .succeeds()
.stdout_only("05-"); .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!() new_ucmd!()
.args(&[ .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", "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",