frost-builtins 0.1.0

Built-in shell commands for frost
Documentation
//! The `print` builtin — zsh-style print command.
//!
//! Supports `-n` (no newline), `-r` (raw/no escapes), `-l` (one arg per line).

use crate::{Builtin, ShellEnvironment};

pub struct Print;

impl Builtin for Print {
    fn name(&self) -> &str {
        "print"
    }

    fn execute(&self, args: &[&str], _env: &mut dyn ShellEnvironment) -> i32 {
        let mut newline = true;
        let mut raw = false;
        let mut one_per_line = false;
        let mut text_start = 0;

        for (i, arg) in args.iter().enumerate() {
            if !arg.starts_with('-') || arg.len() < 2 || *arg == "--" {
                if *arg == "--" {
                    text_start = i + 1;
                }
                break;
            }
            let flag_bytes = &arg.as_bytes()[1..];
            let all_known = flag_bytes
                .iter()
                .all(|b| matches!(b, b'n' | b'r' | b'l' | b'R'));
            if all_known {
                for &b in flag_bytes {
                    match b {
                        b'n' => newline = false,
                        b'r' | b'R' => raw = true,
                        b'l' => one_per_line = true,
                        _ => {}
                    }
                }
                text_start = i + 1;
            } else {
                break;
            }
        }

        let text_args = &args[text_start..];

        if one_per_line {
            for arg in text_args {
                println!("{arg}");
            }
        } else {
            let joined = text_args.join(" ");
            if raw {
                print!("{joined}");
            } else {
                print!("{}", expand_escapes(&joined));
            }
            if newline {
                println!();
            }
        }

        0
    }
}

fn expand_escapes(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let mut chars = s.chars();

    while let Some(c) = chars.next() {
        if c == '\\' {
            match chars.next() {
                Some('n') => out.push('\n'),
                Some('t') => out.push('\t'),
                Some('r') => out.push('\r'),
                Some('a') => out.push('\x07'),
                Some('b') => out.push('\x08'),
                Some('\\') => out.push('\\'),
                Some('e') | Some('E') => out.push('\x1B'),
                Some('c') => break,
                Some(other) => {
                    out.push('\\');
                    out.push(other);
                }
                None => out.push('\\'),
            }
        } else {
            out.push(c);
        }
    }

    out
}