Skip to content

Commit

Permalink
uucore: add sprintf() function
Browse files Browse the repository at this point in the history
Add an `sprintf()` function (and `printf()` function) to
`uucore::memo`.  The `sprintf()` function behaves roughly like the
standard Rust `format!()`, but follows our implementation of the GNU
`printf` formatting logic instead.
  • Loading branch information
jfinkels authored and sylvestre committed Nov 29, 2022
1 parent e307f62 commit 2cb513d
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 105 deletions.
5 changes: 3 additions & 2 deletions src/uu/printf/src/printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

use clap::{crate_version, Arg, ArgAction, Command};
use uucore::error::{UResult, UUsageError};
use uucore::{format_usage, memo};
use uucore::format_usage;
use uucore::memo::printf;

const VERSION: &str = "version";
const HELP: &str = "help";
Expand Down Expand Up @@ -281,7 +282,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
None => vec![],
};

memo::Memo::run_all(format_string, &values[..])?;
printf(format_string, &values[..])?;
Ok(())
}

Expand Down
15 changes: 6 additions & 9 deletions src/uu/seq/src/seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use num_traits::Zero;
use uucore::error::FromIo;
use uucore::error::UResult;
use uucore::format_usage;
use uucore::memo::Memo;
use uucore::memo::printf;
use uucore::show;

mod error;
Expand Down Expand Up @@ -275,13 +275,10 @@ fn print_seq(
// If there was an argument `-f FORMAT`, then use that format
// template instead of the default formatting strategy.
//
// The `Memo::run_all()` function takes in the template and
// the current value and writes the result to `stdout`.
//
// TODO The `run_all()` method takes a string as its second
// TODO The `printf()` method takes a string as its second
// parameter but we have an `ExtendedBigDecimal`. In order to
// satisfy the signature of the function, we convert the
// `ExtendedBigDecimal` into a string. The `Memo::run_all()`
// `ExtendedBigDecimal` into a string. The `printf()`
// logic will subsequently parse that string into something
// similar to an `ExtendedBigDecimal` again before rendering
// it as a string and ultimately writing to `stdout`. We
Expand All @@ -290,7 +287,7 @@ fn print_seq(
match format {
Some(f) => {
let s = format!("{}", value);
if let Err(x) = Memo::run_all(f, &[s]) {
if let Err(x) = printf(f, &[s]) {
show!(x);
exit(1);
}
Expand Down Expand Up @@ -348,14 +345,14 @@ fn print_seq_integers(
// If there was an argument `-f FORMAT`, then use that format
// template instead of the default formatting strategy.
//
// The `Memo::run_all()` function takes in the template and
// The `printf()` function takes in the template and
// the current value and writes the result to `stdout`.
//
// TODO See similar comment about formatting in `print_seq()`.
match format {
Some(f) => {
let s = format!("{}", value);
if let Err(x) = Memo::run_all(f, &[s]) {
if let Err(x) = printf(f, &[s]) {
show!(x);
exit(1);
}
Expand Down
136 changes: 113 additions & 23 deletions src/uucore/src/lib/features/memo.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
//! Memo runner of printf
//! Takes a format string and arguments
//! 1. tokenize format string into tokens, consuming
//! any subst. arguments along the way.
//! 2. feeds remaining arguments into function
//! that prints tokens.
//! Main entry point for our implementation of printf.
//!
//! The [`printf`] and [`sprintf`] closely match the behavior of the
//! corresponding C functions: the former renders a formatted string
//! to stdout, the latter renders to a new [`String`] object.
use crate::display::Quotable;
use crate::error::UResult;
use crate::features::tokenize::sub::Sub;
use crate::features::tokenize::token::{Token, Tokenizer};
use crate::error::{UResult, USimpleError};
use crate::features::tokenize::sub::SubParser;
use crate::features::tokenize::token::Token;
use crate::features::tokenize::unescaped_text::UnescapedText;
use crate::show_warning;
use itertools::put_back_n;
use std::io::{stdout, Cursor, Write};
use std::iter::Peekable;
use std::slice::Iter;

pub struct Memo {
tokens: Vec<Box<dyn Token>>,
/// Memo runner of printf
/// Takes a format string and arguments
/// 1. tokenize format string into tokens, consuming
/// any subst. arguments along the way.
/// 2. feeds remaining arguments into function
/// that prints tokens.
struct Memo {
tokens: Vec<Token>,
}

fn warn_excess_args(first_arg: &str) {
Expand All @@ -27,18 +32,22 @@ fn warn_excess_args(first_arg: &str) {
}

impl Memo {
pub fn new(pf_string: &str, pf_args_it: &mut Peekable<Iter<String>>) -> UResult<Self> {
fn new<W>(
writer: &mut W,
pf_string: &str,
pf_args_it: &mut Peekable<Iter<String>>,
) -> UResult<Self>
where
W: Write,
{
let mut pm = Self { tokens: Vec::new() };
let mut tmp_token: Option<Box<dyn Token>>;
let mut it = put_back_n(pf_string.chars());
let mut has_sub = false;
loop {
tmp_token = UnescapedText::from_it(&mut it, pf_args_it)?;
if let Some(x) = tmp_token {
if let Some(x) = UnescapedText::from_it_core(writer, &mut it, false) {
pm.tokens.push(x);
}
tmp_token = Sub::from_it(&mut it, pf_args_it)?;
if let Some(x) = tmp_token {
if let Some(x) = SubParser::from_it(writer, &mut it, pf_args_it)? {
if !has_sub {
has_sub = true;
}
Expand Down Expand Up @@ -67,19 +76,100 @@ impl Memo {
}
Ok(pm)
}
pub fn apply(&self, pf_args_it: &mut Peekable<Iter<String>>) {
fn apply<W>(&self, writer: &mut W, pf_args_it: &mut Peekable<Iter<String>>)
where
W: Write,
{
for tkn in &self.tokens {
tkn.print(pf_args_it);
tkn.write(writer, pf_args_it);
}
}
pub fn run_all(pf_string: &str, pf_args: &[String]) -> UResult<()> {
fn run_all<W>(writer: &mut W, pf_string: &str, pf_args: &[String]) -> UResult<()>
where
W: Write,
{
let mut arg_it = pf_args.iter().peekable();
let pm = Self::new(pf_string, &mut arg_it)?;
let pm = Self::new(writer, pf_string, &mut arg_it)?;
loop {
if arg_it.peek().is_none() {
return Ok(());
}
pm.apply(&mut arg_it);
pm.apply(writer, &mut arg_it);
}
}
}

/// Write a formatted string to stdout.
///
/// `format_string` contains the template and `args` contains the
/// arguments to render into the template.
///
/// See also [`sprintf`], which creates a new formatted [`String`].
///
/// # Examples
///
/// ```rust
/// use uucore::memo::printf;
///
/// printf("hello %s", &["world".to_string()]).unwrap();
/// // prints "hello world"
/// ```
pub fn printf(format_string: &str, args: &[String]) -> UResult<()> {
let mut writer = stdout();
Memo::run_all(&mut writer, format_string, args)
}

/// Create a new formatted string.
///
/// `format_string` contains the template and `args` contains the
/// arguments to render into the template.
///
/// See also [`printf`], which prints to stdout.
///
/// # Examples
///
/// ```rust
/// use uucore::memo::sprintf;
///
/// let s = sprintf("hello %s", &["world".to_string()]).unwrap();
/// assert_eq!(s, "hello world".to_string());
/// ```
pub fn sprintf(format_string: &str, args: &[String]) -> UResult<String> {
let mut writer = Cursor::new(vec![]);
Memo::run_all(&mut writer, format_string, args)?;
let buf = writer.into_inner();
match String::from_utf8(buf) {
Ok(s) => Ok(s),
Err(e) => Err(USimpleError::new(
1,
format!("failed to parse formatted string as UTF-8: {}", e),
)),
}
}

#[cfg(test)]
mod tests {

use crate::memo::sprintf;

#[test]
fn test_sprintf_smoke() {
assert_eq!(sprintf("", &[]).unwrap(), "".to_string())
}

#[test]
fn test_sprintf_no_args() {
assert_eq!(
sprintf("hello world", &[]).unwrap(),
"hello world".to_string()
)
}

#[test]
fn test_sprintf_string() {
assert_eq!(
sprintf("hello %s", &["world".to_string()]).unwrap(),
"hello world".to_string()
)
}
}
45 changes: 23 additions & 22 deletions src/uucore/src/lib/features/tokenize/sub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::error::{UError, UResult};
use itertools::{put_back_n, PutBackN};
use std::error::Error;
use std::fmt::Display;
use std::io::Write;
use std::iter::Peekable;
use std::process::exit;
use std::slice::Iter;
Expand Down Expand Up @@ -112,7 +113,7 @@ impl Sub {
}

#[derive(Default)]
struct SubParser {
pub(crate) struct SubParser {
min_width_tmp: Option<String>,
min_width_is_asterisk: bool,
past_decimal: bool,
Expand All @@ -127,28 +128,32 @@ impl SubParser {
fn new() -> Self {
Self::default()
}
fn from_it(
pub(crate) fn from_it<W>(
writer: &mut W,
it: &mut PutBackN<Chars>,
args: &mut Peekable<Iter<String>>,
) -> UResult<Option<Box<dyn token::Token>>> {
) -> UResult<Option<token::Token>>
where
W: Write,
{
let mut parser = Self::new();
if parser.sub_vals_retrieved(it)? {
let t: Box<dyn token::Token> = Self::build_token(parser);
t.print(args);
let t = Self::build_token(parser);
t.write(writer, args);
Ok(Some(t))
} else {
Ok(None)
}
}
fn build_token(parser: Self) -> Box<dyn token::Token> {
fn build_token(parser: Self) -> token::Token {
// not a self method so as to allow move of sub-parser vals.
// return new Sub struct as token
let prefix_char = match &parser.min_width_tmp {
Some(width) if width.starts_with('0') => '0',
_ => ' ',
};

let t: Box<dyn token::Token> = Box::new(Sub::new(
token::Token::Sub(Sub::new(
if parser.min_width_is_asterisk {
CanAsterisk::Asterisk
} else {
Expand All @@ -166,8 +171,7 @@ impl SubParser {
parser.field_char.unwrap(),
parser.text_so_far,
prefix_char,
));
t
))
}
fn sub_vals_retrieved(&mut self, it: &mut PutBackN<Chars>) -> UResult<bool> {
if !Self::successfully_eat_prefix(it, &mut self.text_so_far)? {
Expand Down Expand Up @@ -337,16 +341,11 @@ impl SubParser {
}
}

impl token::Tokenizer for Sub {
fn from_it(
it: &mut PutBackN<Chars>,
args: &mut Peekable<Iter<String>>,
) -> UResult<Option<Box<dyn token::Token>>> {
SubParser::from_it(it, args)
}
}
impl token::Token for Sub {
fn print(&self, pf_args_it: &mut Peekable<Iter<String>>) {
impl Sub {
pub(crate) fn write<W>(&self, writer: &mut W, pf_args_it: &mut Peekable<Iter<String>>)
where
W: Write,
{
let field = FormatField {
min_width: match self.min_width {
CanAsterisk::Fixed(x) => x,
Expand Down Expand Up @@ -397,7 +396,7 @@ impl token::Token for Sub {
}),
'b' => {
let mut a_it = put_back_n(arg_string.chars());
UnescapedText::from_it_core(&mut a_it, true);
UnescapedText::from_it_core(writer, &mut a_it, true);
None
}
// for 'c': get iter of string vals,
Expand All @@ -417,7 +416,8 @@ impl token::Token for Sub {
};
if let Some(pre_min_width) = pre_min_width_opt {
// if have a string, print it, ensuring minimum width is met.
print!(
write!(
writer,
"{}",
match field.min_width {
Some(min_width) => {
Expand All @@ -443,7 +443,8 @@ impl token::Token for Sub {
}
None => pre_min_width,
}
);
)
.ok();
}
}
}
Loading

0 comments on commit 2cb513d

Please sign in to comment.