Skip to main content

coreutils_rs/printf/
core.rs

1/// GNU coreutils-compatible printf implementation.
2///
3/// Processes a printf format string with the given arguments, returning the
4/// raw output bytes. The format string is reused if there are more arguments
5/// than a single pass consumes.
6
7/// Sentinel returned inside `process_format_string` when `\c` is encountered.
8const STOP_OUTPUT: u8 = 0xFF;
9
10/// Process a printf format string with the given arguments, returning raw bytes.
11///
12/// The format string repeats if there are more arguments than one pass consumes.
13/// Processing stops immediately when `\c` is encountered (in the format string
14/// itself or inside a `%b` argument).
15pub fn process_format_string(format: &str, args: &[&str]) -> Vec<u8> {
16    let mut output = Vec::with_capacity(256);
17    let fmt_bytes = format.as_bytes();
18
19    if args.is_empty() {
20        // Single pass with no arguments
21        let stop = format_one_pass(fmt_bytes, args, &mut 0, &mut output);
22        if stop {
23            // remove trailing STOP_OUTPUT sentinel if present
24            if output.last() == Some(&STOP_OUTPUT) {
25                output.pop();
26            }
27        }
28        return output;
29    }
30
31    let mut arg_idx: usize = 0;
32    loop {
33        let start_idx = arg_idx;
34        let stop = format_one_pass(fmt_bytes, args, &mut arg_idx, &mut output);
35        if stop {
36            if output.last() == Some(&STOP_OUTPUT) {
37                output.pop();
38            }
39            break;
40        }
41        // If no arguments were consumed, or we've used them all, stop
42        if arg_idx == start_idx || arg_idx >= args.len() {
43            break;
44        }
45    }
46
47    output
48}
49
50/// Run one pass of the format string. Returns `true` if output should stop (`\c`).
51/// `arg_idx` is advanced as arguments are consumed.
52fn format_one_pass(fmt: &[u8], args: &[&str], arg_idx: &mut usize, output: &mut Vec<u8>) -> bool {
53    let mut i = 0;
54    while i < fmt.len() {
55        match fmt[i] {
56            b'%' => {
57                i += 1;
58                if i >= fmt.len() {
59                    output.push(b'%');
60                    break;
61                }
62                if fmt[i] == b'%' {
63                    output.push(b'%');
64                    i += 1;
65                    continue;
66                }
67                let stop = process_conversion(fmt, &mut i, args, arg_idx, output);
68                if stop {
69                    return true;
70                }
71            }
72            b'\\' => {
73                i += 1;
74                let stop = process_format_escape(fmt, &mut i, output);
75                if stop {
76                    return true;
77                }
78            }
79            ch => {
80                output.push(ch);
81                i += 1;
82            }
83        }
84    }
85    false
86}
87
88/// Process a conversion specifier (the part after `%`).
89/// `i` points to the first character after `%`. Returns true if `\c` stop was hit.
90fn process_conversion(
91    fmt: &[u8],
92    i: &mut usize,
93    args: &[&str],
94    arg_idx: &mut usize,
95    output: &mut Vec<u8>,
96) -> bool {
97    // Parse flags
98    let mut flags = FormatFlags::default();
99    while *i < fmt.len() {
100        match fmt[*i] {
101            b'-' => flags.left_align = true,
102            b'+' => flags.plus_sign = true,
103            b' ' => flags.space_sign = true,
104            b'0' => flags.zero_pad = true,
105            b'#' => flags.alternate = true,
106            _ => break,
107        }
108        *i += 1;
109    }
110
111    // Parse width
112    let width = parse_decimal(fmt, i);
113
114    // Parse precision
115    let precision = if *i < fmt.len() && fmt[*i] == b'.' {
116        *i += 1;
117        Some(parse_decimal(fmt, i))
118    } else {
119        None
120    };
121
122    // Parse conversion character
123    if *i >= fmt.len() {
124        return false;
125    }
126    let conv = fmt[*i];
127    *i += 1;
128
129    let arg = consume_arg(args, arg_idx);
130
131    match conv {
132        b's' => {
133            let s = arg;
134            let formatted = apply_string_format(s, &flags, width, precision);
135            output.extend_from_slice(&formatted);
136        }
137        b'b' => {
138            let (bytes, stop) = process_b_argument(arg);
139            let formatted = apply_string_format_bytes(&bytes, &flags, width, precision);
140            output.extend_from_slice(&formatted);
141            if stop {
142                return true;
143            }
144        }
145        b'c' => {
146            if let Some(ch) = arg.chars().next() {
147                let mut buf = [0u8; 4];
148                let encoded = ch.encode_utf8(&mut buf);
149                let formatted = apply_string_format(encoded, &flags, width, precision);
150                output.extend_from_slice(&formatted);
151            } else {
152                // empty arg: output a NUL byte (GNU compat)
153                let formatted = apply_string_format_bytes(&[0], &flags, width, precision);
154                output.extend_from_slice(&formatted);
155            }
156        }
157        b'd' | b'i' => {
158            let val = parse_integer(arg);
159            let s = format!("{}", val);
160            let formatted = apply_numeric_format(&s, val < 0, &flags, width, precision);
161            output.extend_from_slice(formatted.as_bytes());
162        }
163        b'u' => {
164            let val = parse_unsigned(arg);
165            let s = format!("{}", val);
166            let formatted = apply_numeric_format(&s, false, &flags, width, precision);
167            output.extend_from_slice(formatted.as_bytes());
168        }
169        b'o' => {
170            let val = parse_unsigned(arg);
171            let s = format!("{:o}", val);
172            let prefix = if flags.alternate && !s.starts_with('0') {
173                "0"
174            } else {
175                ""
176            };
177            let full = format!("{}{}", prefix, s);
178            let formatted = apply_numeric_format(&full, false, &flags, width, precision);
179            output.extend_from_slice(formatted.as_bytes());
180        }
181        b'x' => {
182            let val = parse_unsigned(arg);
183            let s = format!("{:x}", val);
184            let prefix = if flags.alternate && val != 0 {
185                "0x"
186            } else {
187                ""
188            };
189            let full = format!("{}{}", prefix, s);
190            let formatted = apply_numeric_format(&full, false, &flags, width, precision);
191            output.extend_from_slice(formatted.as_bytes());
192        }
193        b'X' => {
194            let val = parse_unsigned(arg);
195            let s = format!("{:X}", val);
196            let prefix = if flags.alternate && val != 0 {
197                "0X"
198            } else {
199                ""
200            };
201            let full = format!("{}{}", prefix, s);
202            let formatted = apply_numeric_format(&full, false, &flags, width, precision);
203            output.extend_from_slice(formatted.as_bytes());
204        }
205        b'f' => {
206            let val = parse_float(arg);
207            let prec = precision.unwrap_or(6);
208            let s = format!("{:.prec$}", val, prec = prec);
209            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
210            output.extend_from_slice(formatted.as_bytes());
211        }
212        b'e' => {
213            let val = parse_float(arg);
214            let prec = precision.unwrap_or(6);
215            let s = format_scientific(val, prec, 'e');
216            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
217            output.extend_from_slice(formatted.as_bytes());
218        }
219        b'E' => {
220            let val = parse_float(arg);
221            let prec = precision.unwrap_or(6);
222            let s = format_scientific(val, prec, 'E');
223            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
224            output.extend_from_slice(formatted.as_bytes());
225        }
226        b'g' => {
227            let val = parse_float(arg);
228            let prec = precision.unwrap_or(6);
229            let s = format_g(val, prec, false);
230            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
231            output.extend_from_slice(formatted.as_bytes());
232        }
233        b'G' => {
234            let val = parse_float(arg);
235            let prec = precision.unwrap_or(6);
236            let s = format_g(val, prec, true);
237            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
238            output.extend_from_slice(formatted.as_bytes());
239        }
240        b'q' => {
241            let s = arg;
242            let quoted = shell_quote(s);
243            let formatted = apply_string_format(&quoted, &flags, width, precision);
244            output.extend_from_slice(&formatted);
245        }
246        _ => {
247            // Unknown conversion: output literally
248            output.push(b'%');
249            output.push(conv);
250        }
251    }
252    false
253}
254
255/// Consume the next argument, returning "" if exhausted.
256fn consume_arg<'a>(args: &[&'a str], arg_idx: &mut usize) -> &'a str {
257    if *arg_idx < args.len() {
258        let val = args[*arg_idx];
259        *arg_idx += 1;
260        val
261    } else {
262        ""
263    }
264}
265
266/// Process an escape sequence in the format string.
267/// `i` points to the character after `\`. Returns true if `\c` was encountered.
268fn process_format_escape(fmt: &[u8], i: &mut usize, output: &mut Vec<u8>) -> bool {
269    if *i >= fmt.len() {
270        output.push(b'\\');
271        return false;
272    }
273    match fmt[*i] {
274        b'\\' => {
275            output.push(b'\\');
276            *i += 1;
277        }
278        b'"' => {
279            output.push(b'"');
280            *i += 1;
281        }
282        b'a' => {
283            output.push(0x07);
284            *i += 1;
285        }
286        b'b' => {
287            output.push(0x08);
288            *i += 1;
289        }
290        b'c' => {
291            return true;
292        }
293        b'e' | b'E' => {
294            output.push(0x1B);
295            *i += 1;
296        }
297        b'f' => {
298            output.push(0x0C);
299            *i += 1;
300        }
301        b'n' => {
302            output.push(b'\n');
303            *i += 1;
304        }
305        b'r' => {
306            output.push(b'\r');
307            *i += 1;
308        }
309        b't' => {
310            output.push(b'\t');
311            *i += 1;
312        }
313        b'v' => {
314            output.push(0x0B);
315            *i += 1;
316        }
317        b'0' => {
318            // Octal: \0NNN (up to 3 octal digits after the leading 0)
319            *i += 1;
320            let val = parse_octal_digits(fmt, i, 3);
321            output.push(val);
322        }
323        b'1'..=b'7' => {
324            // Octal: \NNN (up to 3 octal digits)
325            let val = parse_octal_digits(fmt, i, 3);
326            output.push(val);
327        }
328        b'x' => {
329            *i += 1;
330            let val = parse_hex_digits(fmt, i, 2);
331            output.push(val as u8);
332        }
333        b'u' => {
334            *i += 1;
335            let val = parse_hex_digits(fmt, i, 4);
336            if let Some(ch) = char::from_u32(val) {
337                let mut buf = [0u8; 4];
338                let encoded = ch.encode_utf8(&mut buf);
339                output.extend_from_slice(encoded.as_bytes());
340            }
341        }
342        b'U' => {
343            *i += 1;
344            let val = parse_hex_digits(fmt, i, 8);
345            if let Some(ch) = char::from_u32(val) {
346                let mut buf = [0u8; 4];
347                let encoded = ch.encode_utf8(&mut buf);
348                output.extend_from_slice(encoded.as_bytes());
349            }
350        }
351        _ => {
352            // Unknown escape: output backslash and the character
353            output.push(b'\\');
354            output.push(fmt[*i]);
355            *i += 1;
356        }
357    }
358    false
359}
360
361/// Process backslash escapes in a %b argument string.
362/// Returns (bytes, stop) where stop is true if \c was found.
363fn process_b_argument(arg: &str) -> (Vec<u8>, bool) {
364    let bytes = arg.as_bytes();
365    let mut output = Vec::with_capacity(bytes.len());
366    let mut i = 0;
367    while i < bytes.len() {
368        if bytes[i] == b'\\' {
369            i += 1;
370            if i >= bytes.len() {
371                output.push(b'\\');
372                break;
373            }
374            match bytes[i] {
375                b'\\' => {
376                    output.push(b'\\');
377                    i += 1;
378                }
379                b'a' => {
380                    output.push(0x07);
381                    i += 1;
382                }
383                b'b' => {
384                    output.push(0x08);
385                    i += 1;
386                }
387                b'c' => {
388                    return (output, true);
389                }
390                b'e' | b'E' => {
391                    output.push(0x1B);
392                    i += 1;
393                }
394                b'f' => {
395                    output.push(0x0C);
396                    i += 1;
397                }
398                b'n' => {
399                    output.push(b'\n');
400                    i += 1;
401                }
402                b'r' => {
403                    output.push(b'\r');
404                    i += 1;
405                }
406                b't' => {
407                    output.push(b'\t');
408                    i += 1;
409                }
410                b'v' => {
411                    output.push(0x0B);
412                    i += 1;
413                }
414                b'0' => {
415                    i += 1;
416                    let val = parse_octal_digits(bytes, &mut i, 3);
417                    output.push(val);
418                }
419                b'1'..=b'7' => {
420                    let val = parse_octal_digits(bytes, &mut i, 3);
421                    output.push(val);
422                }
423                b'x' => {
424                    i += 1;
425                    let val = parse_hex_digits(bytes, &mut i, 2);
426                    output.push(val as u8);
427                }
428                _ => {
429                    // In %b, unknown escapes pass through literally
430                    output.push(b'\\');
431                    output.push(bytes[i]);
432                    i += 1;
433                }
434            }
435        } else {
436            output.push(bytes[i]);
437            i += 1;
438        }
439    }
440    (output, false)
441}
442
443/// Parse up to `max_digits` octal digits from `data` starting at `*i`.
444fn parse_octal_digits(data: &[u8], i: &mut usize, max_digits: usize) -> u8 {
445    let mut val: u32 = 0;
446    let mut count = 0;
447    while *i < data.len() && count < max_digits {
448        let ch = data[*i];
449        if ch >= b'0' && ch <= b'7' {
450            val = val * 8 + (ch - b'0') as u32;
451            *i += 1;
452            count += 1;
453        } else {
454            break;
455        }
456    }
457    (val & 0xFF) as u8
458}
459
460/// Parse up to `max_digits` hex digits from `data` starting at `*i`.
461fn parse_hex_digits(data: &[u8], i: &mut usize, max_digits: usize) -> u32 {
462    let mut val: u32 = 0;
463    let mut count = 0;
464    while *i < data.len() && count < max_digits {
465        let ch = data[*i];
466        if ch.is_ascii_hexdigit() {
467            val = val * 16 + hex_digit_value(ch) as u32;
468            *i += 1;
469            count += 1;
470        } else {
471            break;
472        }
473    }
474    val
475}
476
477fn hex_digit_value(ch: u8) -> u8 {
478    match ch {
479        b'0'..=b'9' => ch - b'0',
480        b'a'..=b'f' => ch - b'a' + 10,
481        b'A'..=b'F' => ch - b'A' + 10,
482        _ => 0,
483    }
484}
485
486/// Parse a decimal integer from `data` at position `*i`.
487fn parse_decimal(data: &[u8], i: &mut usize) -> usize {
488    let mut val: usize = 0;
489    while *i < data.len() && data[*i].is_ascii_digit() {
490        val = val
491            .saturating_mul(10)
492            .saturating_add((data[*i] - b'0') as usize);
493        *i += 1;
494    }
495    val
496}
497
498/// Parse an integer argument. Supports decimal, octal (0-prefix), hex (0x-prefix),
499/// and single-character constants ('c' or "c").
500fn parse_integer(s: &str) -> i64 {
501    let s = s.trim();
502    if s.is_empty() {
503        return 0;
504    }
505
506    // Character constants: 'X or "X
507    if (s.starts_with('\'') || s.starts_with('"')) && s.len() >= 2 {
508        return s[1..].chars().next().map_or(0, |c| c as i64);
509    }
510
511    // Try to detect sign
512    let (negative, digits) = if let Some(rest) = s.strip_prefix('-') {
513        (true, rest)
514    } else if let Some(rest) = s.strip_prefix('+') {
515        (false, rest)
516    } else {
517        (false, s)
518    };
519
520    let magnitude = if let Some(hex) = digits
521        .strip_prefix("0x")
522        .or_else(|| digits.strip_prefix("0X"))
523    {
524        u64::from_str_radix(hex, 16).unwrap_or(0)
525    } else if let Some(oct) = digits.strip_prefix('0') {
526        if oct.is_empty() {
527            0
528        } else {
529            u64::from_str_radix(oct, 8).unwrap_or(0)
530        }
531    } else {
532        digits.parse::<u64>().unwrap_or(0)
533    };
534
535    if negative {
536        -(magnitude as i64)
537    } else {
538        magnitude as i64
539    }
540}
541
542/// Parse an unsigned integer argument.
543fn parse_unsigned(s: &str) -> u64 {
544    let s = s.trim();
545    if s.is_empty() {
546        return 0;
547    }
548
549    // Character constants
550    if (s.starts_with('\'') || s.starts_with('"')) && s.len() >= 2 {
551        return s[1..].chars().next().map_or(0, |c| c as u64);
552    }
553
554    // Negative values wrap around like C unsigned
555    let (negative, digits) = if let Some(rest) = s.strip_prefix('-') {
556        (true, rest)
557    } else if let Some(rest) = s.strip_prefix('+') {
558        (false, rest)
559    } else {
560        (false, s)
561    };
562
563    let magnitude = if let Some(hex) = digits
564        .strip_prefix("0x")
565        .or_else(|| digits.strip_prefix("0X"))
566    {
567        u64::from_str_radix(hex, 16).unwrap_or(0)
568    } else if let Some(oct) = digits.strip_prefix('0') {
569        if oct.is_empty() {
570            0
571        } else {
572            u64::from_str_radix(oct, 8).unwrap_or(0)
573        }
574    } else {
575        digits.parse::<u64>().unwrap_or(0)
576    };
577
578    if negative {
579        magnitude.wrapping_neg()
580    } else {
581        magnitude
582    }
583}
584
585/// Parse a floating-point argument.
586fn parse_float(s: &str) -> f64 {
587    let s = s.trim();
588    if s.is_empty() {
589        return 0.0;
590    }
591
592    // Character constants
593    if (s.starts_with('\'') || s.starts_with('"')) && s.len() >= 2 {
594        return s[1..].chars().next().map_or(0.0, |c| c as u32 as f64);
595    }
596
597    // Handle hex float prefix for parsing
598    if s.starts_with("0x") || s.starts_with("0X") || s.starts_with("-0x") || s.starts_with("-0X") {
599        // Rust doesn't parse hex floats natively; parse as integer
600        return parse_integer(s) as f64;
601    }
602
603    s.parse::<f64>().unwrap_or(0.0)
604}
605
606#[derive(Default)]
607struct FormatFlags {
608    left_align: bool,
609    plus_sign: bool,
610    space_sign: bool,
611    zero_pad: bool,
612    alternate: bool,
613}
614
615/// Apply string formatting with width and precision (for %s, %b, %c).
616fn apply_string_format(
617    s: &str,
618    flags: &FormatFlags,
619    width: usize,
620    precision: Option<usize>,
621) -> Vec<u8> {
622    let truncated: &str;
623    let owned: String;
624    if let Some(prec) = precision {
625        if s.len() > prec {
626            // Truncate to prec bytes, but respect UTF-8 boundaries
627            owned = s.chars().take(prec).collect();
628            truncated = &owned;
629        } else {
630            truncated = s;
631        }
632    } else {
633        truncated = s;
634    }
635
636    apply_padding(truncated.as_bytes(), flags, width)
637}
638
639/// Apply string formatting for raw bytes.
640fn apply_string_format_bytes(
641    s: &[u8],
642    flags: &FormatFlags,
643    width: usize,
644    precision: Option<usize>,
645) -> Vec<u8> {
646    let data = if let Some(prec) = precision {
647        if s.len() > prec { &s[..prec] } else { s }
648    } else {
649        s
650    };
651
652    apply_padding(data, flags, width)
653}
654
655/// Apply padding (left or right) to reach the desired width.
656fn apply_padding(data: &[u8], flags: &FormatFlags, width: usize) -> Vec<u8> {
657    if width == 0 || data.len() >= width {
658        return data.to_vec();
659    }
660    let pad_len = width - data.len();
661    let mut result = Vec::with_capacity(width);
662    if flags.left_align {
663        result.extend_from_slice(data);
664        result.resize(result.len() + pad_len, b' ');
665    } else {
666        result.resize(pad_len, b' ');
667        result.extend_from_slice(data);
668    }
669    result
670}
671
672/// Apply numeric formatting with width, flags, and optional precision for integers.
673fn apply_numeric_format(
674    num_str: &str,
675    is_negative: bool,
676    flags: &FormatFlags,
677    width: usize,
678    precision: Option<usize>,
679) -> String {
680    // For integers, precision specifies minimum number of digits
681    let digits = if is_negative {
682        &num_str[1..] // strip the minus
683    } else {
684        num_str
685    };
686
687    let digits = if let Some(prec) = precision {
688        if prec > 0 && digits.len() < prec {
689            let padding = "0".repeat(prec - digits.len());
690            format!("{}{}", padding, digits)
691        } else if prec == 0 && digits == "0" {
692            String::new()
693        } else {
694            digits.to_string()
695        }
696    } else {
697        digits.to_string()
698    };
699
700    let sign = if is_negative {
701        "-".to_string()
702    } else if flags.plus_sign {
703        "+".to_string()
704    } else if flags.space_sign {
705        " ".to_string()
706    } else {
707        String::new()
708    };
709
710    let content = format!("{}{}", sign, digits);
711
712    if width > 0 && content.len() < width {
713        let pad_len = width - content.len();
714        if flags.left_align {
715            format!("{}{}", content, " ".repeat(pad_len))
716        } else if flags.zero_pad && precision.is_none() {
717            format!("{}{}{}", sign, "0".repeat(pad_len), digits)
718        } else {
719            format!("{}{}", " ".repeat(pad_len), content)
720        }
721    } else {
722        content
723    }
724}
725
726/// Apply float formatting with width and flags.
727fn apply_float_format(
728    num_str: &str,
729    _is_negative: bool,
730    flags: &FormatFlags,
731    width: usize,
732) -> String {
733    let (sign_prefix, abs_str) = if num_str.starts_with('-') {
734        ("-", &num_str[1..])
735    } else if flags.plus_sign {
736        ("+", num_str)
737    } else if flags.space_sign {
738        (" ", num_str)
739    } else {
740        ("", num_str)
741    };
742
743    let content = format!("{}{}", sign_prefix, abs_str);
744
745    if width > 0 && content.len() < width {
746        let pad_len = width - content.len();
747        if flags.left_align {
748            format!("{}{}", content, " ".repeat(pad_len))
749        } else if flags.zero_pad {
750            format!("{}{}{}", sign_prefix, "0".repeat(pad_len), abs_str)
751        } else {
752            format!("{}{}", " ".repeat(pad_len), content)
753        }
754    } else {
755        content
756    }
757}
758
759/// Format in scientific notation matching C's %e.
760fn format_scientific(value: f64, prec: usize, e_char: char) -> String {
761    if value == 0.0 {
762        let sign = if value.is_sign_negative() { "-" } else { "" };
763        if prec == 0 {
764            return format!("{sign}0{e_char}+00");
765        }
766        return format!("{sign}0.{:0>prec$}{e_char}+00", "", prec = prec);
767    }
768
769    let abs = value.abs();
770    let sign = if value < 0.0 { "-" } else { "" };
771    let exp = abs.log10().floor() as i32;
772    let mantissa = abs / 10f64.powi(exp);
773
774    let factor = 10f64.powi(prec as i32);
775    let mantissa = (mantissa * factor).round() / factor;
776
777    let (mantissa, exp) = if mantissa >= 10.0 {
778        (mantissa / 10.0, exp + 1)
779    } else {
780        (mantissa, exp)
781    };
782
783    let exp_sign = if exp >= 0 { '+' } else { '-' };
784    let exp_abs = exp.unsigned_abs();
785
786    if prec == 0 {
787        format!("{sign}{mantissa:.0}{e_char}{exp_sign}{exp_abs:02}")
788    } else {
789        format!(
790            "{sign}{mantissa:.prec$}{e_char}{exp_sign}{exp_abs:02}",
791            prec = prec
792        )
793    }
794}
795
796/// Format using %g - shortest representation.
797fn format_g(value: f64, prec: usize, upper: bool) -> String {
798    let prec = if prec == 0 { 1 } else { prec };
799
800    if value == 0.0 {
801        let sign = if value.is_sign_negative() { "-" } else { "" };
802        return format!("{sign}0");
803    }
804
805    let abs = value.abs();
806    let exp = abs.log10().floor() as i32;
807    let e_char = if upper { 'E' } else { 'e' };
808
809    if exp < -4 || exp >= prec as i32 {
810        let sig_prec = prec.saturating_sub(1);
811        let s = format_scientific(value, sig_prec, e_char);
812        trim_g_trailing_zeros(&s)
813    } else {
814        let decimal_prec = if prec as i32 > exp + 1 {
815            (prec as i32 - exp - 1) as usize
816        } else {
817            0
818        };
819        let s = format!("{value:.decimal_prec$}");
820        trim_g_trailing_zeros(&s)
821    }
822}
823
824/// Shell-quote a string for %q format specifier (GNU printf compat).
825/// Uses backslash escaping for special characters, or $'...' for control chars.
826fn shell_quote(s: &str) -> String {
827    if s.is_empty() {
828        return "''".to_string();
829    }
830
831    // Check if the string needs quoting at all.
832    let needs_quoting = s.bytes().any(|b| {
833        !b.is_ascii_alphanumeric()
834            && b != b'_'
835            && b != b'/'
836            && b != b'.'
837            && b != b'-'
838            && b != b':'
839            && b != b','
840            && b != b'+'
841            && b != b'@'
842            && b != b'%'
843    });
844
845    if !needs_quoting {
846        return s.to_string();
847    }
848
849    // Check if we have control characters that need $'...' quoting.
850    let has_control = s.bytes().any(|b| b < 0x20 || b == 0x7f || b >= 0x80);
851
852    if has_control {
853        let mut result = String::from("$'");
854        for byte in s.bytes() {
855            match byte {
856                b'\'' => result.push_str("\\'"),
857                b'\\' => result.push_str("\\\\"),
858                b'\n' => result.push_str("\\n"),
859                b'\t' => result.push_str("\\t"),
860                b'\r' => result.push_str("\\r"),
861                0x07 => result.push_str("\\a"),
862                0x08 => result.push_str("\\b"),
863                0x0c => result.push_str("\\f"),
864                0x0b => result.push_str("\\v"),
865                0x1b => result.push_str("\\E"),
866                b if b < 0x20 || b == 0x7f => {
867                    result.push_str(&format!("\\{:03o}", b));
868                }
869                b if b >= 0x80 => {
870                    result.push_str(&format!("\\{:03o}", b));
871                }
872                _ => result.push(byte as char),
873            }
874        }
875        result.push('\'');
876        result
877    } else {
878        // Use backslash escaping for simple special chars.
879        let mut result = String::with_capacity(s.len() * 2);
880        for ch in s.chars() {
881            match ch {
882                ' ' | '\'' | '"' | '\\' | '$' | '`' | '!' | '(' | ')' | '[' | ']' | '{' | '}'
883                | '<' | '>' | '|' | '&' | ';' | '#' | '~' | '?' | '*' | '=' | '^' => {
884                    result.push('\\');
885                    result.push(ch);
886                }
887                _ => result.push(ch),
888            }
889        }
890        result
891    }
892}
893
894/// Trim trailing zeros from %g formatted output.
895/// Only trims after a decimal point to avoid turning "100000" into "1".
896fn trim_g_trailing_zeros(s: &str) -> String {
897    if let Some(e_pos) = s.find(['e', 'E']) {
898        let (mantissa, exponent) = s.split_at(e_pos);
899        if mantissa.contains('.') {
900            let trimmed = mantissa.trim_end_matches('0').trim_end_matches('.');
901            format!("{trimmed}{exponent}")
902        } else {
903            s.to_string()
904        }
905    } else if s.contains('.') {
906        s.trim_end_matches('0').trim_end_matches('.').to_string()
907    } else {
908        s.to_string()
909    }
910}