printable-shell-command 0.2.4

A helper library to print shell commands.
Documentation
use itertools::Itertools;

use crate::{
    format::{conditional_escape, unconditional_escape, ConditionalEscapeOptions},
    ArgumentLineWrapping, FormattingOptions,
};

const DEFAULT_MAIN_INDENTATION: &str = "";
const DEFAULT_ARG_INDENTATION: &str = "  ";

const INLINE_SEPARATOR: &str = " ";
const LINE_WRAP_LINE_END: &str = " \\\n";

struct CachedFormattingInfo {
    formatting_options: FormattingOptions,

    // TODO: construct lazily for perf?
    main_indentation: String,
    // arg_indentation: String,
    // line_wrap_separator: String,
    arg_tuple_separator: String,
    post_command_separator: String,
    entry_separator: String,
}

impl CachedFormattingInfo {
    pub fn new(formatting_options: FormattingOptions) -> Self {
        let main_indentation = formatting_options
            .main_indentation
            .clone()
            .unwrap_or(DEFAULT_MAIN_INDENTATION.to_owned());
        let arg_indentation = formatting_options
            .arg_indentation
            .clone()
            .unwrap_or(DEFAULT_ARG_INDENTATION.to_owned());
        let line_wrap_separator = format!("{}{}", LINE_WRAP_LINE_END, arg_indentation);
        let arg_tuple_separator = match formatting_options
            .argument_line_wrapping
            .unwrap_or_default()
        {
            ArgumentLineWrapping::ByEntry => INLINE_SEPARATOR.to_owned(),
            ArgumentLineWrapping::NestedByEntry => {
                format!("{}{}", line_wrap_separator, arg_indentation)
            }
            ArgumentLineWrapping::ByArgument => line_wrap_separator.clone(),
            ArgumentLineWrapping::Inline => INLINE_SEPARATOR.to_owned(),
        };
        let entry_separator = match formatting_options
            .argument_line_wrapping
            .unwrap_or_default()
        {
            ArgumentLineWrapping::ByEntry
            | ArgumentLineWrapping::NestedByEntry
            | ArgumentLineWrapping::ByArgument => {
                format!(
                    "{}{}{}",
                    LINE_WRAP_LINE_END, main_indentation, arg_indentation
                )
            }
            ArgumentLineWrapping::Inline => INLINE_SEPARATOR.to_owned(),
        };
        let post_command_separator = if formatting_options
            .skip_line_wrap_before_first_arg
            .unwrap_or(false)
        {
            INLINE_SEPARATOR.to_owned()
        } else {
            entry_separator.clone()
        };
        Self {
            formatting_options,
            main_indentation,
            // arg_indentation,
            // line_wrap_separator,
            arg_tuple_separator,
            entry_separator,
            post_command_separator,
        }
    }

    fn format_program_name(&self, program_name: &str) -> String {
        self.escape_arglike(program_name, true)
    }

    fn escape_arglike(&self, arglike: &str, is_main_command: bool) -> String {
        match self.formatting_options.quoting.unwrap_or_default() {
            crate::Quoting::Auto => {
                conditional_escape(arglike, ConditionalEscapeOptions { is_main_command })
            }
            crate::Quoting::ExtraSafe => unconditional_escape(arglike),
        }
    }
}

pub(crate) struct PrintBuilder {
    program_entry: String,
    serialized_entries: Vec<String>,
    cached_formatting_info: CachedFormattingInfo,
}

impl PrintBuilder {
    pub fn new(program_name: &str, formatting_options: FormattingOptions) -> Self {
        let cached_formatting_info = CachedFormattingInfo::new(formatting_options);
        let program_entry = format!(
            "{}{}",
            cached_formatting_info.main_indentation,
            cached_formatting_info.format_program_name(program_name),
        );
        Self {
            program_entry,
            serialized_entries: vec![],
            cached_formatting_info,
        }
    }

    pub fn add_single_arg(&mut self, arg: &str) {
        self.serialized_entries
            .push(self.cached_formatting_info.escape_arglike(arg, false));
    }

    pub fn add_arg_group<T: AsRef<str>>(&mut self, args: impl Iterator<Item = T>) {
        self.serialized_entries.push(
            args.map(|arg| {
                self.cached_formatting_info
                    .escape_arglike(arg.as_ref(), false)
            })
            .join(&self.cached_formatting_info.arg_tuple_separator),
        );
    }

    pub fn get(&self) -> String {
        let mut output = self.program_entry.clone();
        if !self.serialized_entries.is_empty() {
            output = output
                + &self.cached_formatting_info.post_command_separator
                + &self
                    .serialized_entries
                    .join(&self.cached_formatting_info.entry_separator);
        };
        output
    }
}