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