use std::{fmt::Write as _, io::IsTerminal as _};
use crate::{
__private::{
Action, Arg, Command, Flag, HELP_ACTION, HelpGroup, Key, Named, SCHEMA_ACTION,
resolve_long, resolve_short,
},
error::display_bytes,
};
type HelpRows = Vec<(String, String)>;
type GroupedHelp<'a> = Vec<(&'a str, HelpRows)>;
struct VisibleFlag<'a> {
scope: usize,
flag: &'a Flag<'a>,
longs: Vec<&'a str>,
shorts: Vec<u8>,
}
impl<'a> VisibleFlag<'a> {
fn collect(path: &[&'a Command<'a>]) -> Vec<Self> {
let Some((&command, ancestors)) = path.split_last() else {
return Vec::new();
};
let current = ancestors.len();
let candidates = command.flags.iter().copied().map(|flag| (current, flag)).chain(
ancestors.iter().enumerate().rev().flat_map(|(scope, command)| {
command
.flags
.iter()
.copied()
.filter(|flag| flag.global)
.map(move |flag| (scope, flag))
}),
);
candidates
.filter_map(|(scope, flag)| {
let longs = flag
.longs
.iter()
.copied()
.filter(|long| {
matches!(
resolve_long(command, ancestors, long.as_bytes()),
Some(Named::Flag { flag: resolved, scope: resolved_scope })
if resolved_scope == scope && std::ptr::eq(resolved, flag)
)
})
.collect::<Vec<_>>();
let shorts = flag
.shorts
.iter()
.copied()
.filter(|short| {
matches!(
resolve_short(command, ancestors, *short),
Some(Named::Flag { flag: resolved, scope: resolved_scope })
if resolved_scope == scope && std::ptr::eq(resolved, flag)
)
})
.collect::<Vec<_>>();
(!longs.is_empty() || !shorts.is_empty()).then_some(Self {
scope,
flag,
longs,
shorts,
})
})
.collect()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum HelpStyle {
Short,
Long,
}
impl HelpStyle {
fn text<'a>(self, short: Option<&'a str>, long: Option<&'a str>) -> Option<&'a str> {
match self {
Self::Short => short,
Self::Long => long.or(short),
}
}
fn action_help(self, action: &Action<'_>) -> String {
if !matches!(action.kind, crate::__private::ActionKind::Help) {
return action.help.to_owned();
}
match self {
Self::Short => "Print help (see more with '--help')".to_owned(),
Self::Long => "Print help (see a summary with '-h')".to_owned(),
}
}
fn arg_row(self, arg: &Arg<'_>) -> (String, String) {
(
arg_usage(arg),
metadata_help(self.text(arg.help, arg.long_help), arg.accepted_values, None, self),
)
}
fn flag_row(self, flag: &VisibleFlag<'_>) -> (String, String) {
(
spellings_label(
&flag.shorts,
&flag.longs,
flag.flag.takes_value.then_some(flag.flag.name),
),
metadata_help(
self.text(flag.flag.help, flag.flag.long_help),
flag.flag.accepted_values,
flag.flag.default_value,
self,
),
)
}
}
pub(crate) fn render_schema(root: &Command<'_>) -> String {
let mut output = String::new();
output.push_str("Print machine-readable schema\n\n");
output.push_str("Usage: ");
output.push_str(root.name);
output.push_str(" schema [COMMAND]... [--full]\n\n");
output.push_str("Arguments:\n");
write_rows(
&mut output,
&[("[COMMAND]...".to_owned(), "Command path to inspect".to_owned())],
HelpStyle::Short,
);
output.push_str("\nOptions:\n");
write_rows(
&mut output,
&[
("--full".to_owned(), "Recursively expand structural commands".to_owned()),
(
spellings_label(HELP_ACTION.shorts, HELP_ACTION.longs, None),
HELP_ACTION.help.to_owned(),
),
],
HelpStyle::Short,
);
if styling_enabled() { style_headings(&output) } else { output }
}
#[cfg(test)]
pub(crate) fn render(path: &[&Command<'_>]) -> String {
render_with_schema(path, false, HelpStyle::Short)
}
pub(crate) fn render_with_schema(
path: &[&Command<'_>],
schema_enabled: bool,
style: HelpStyle,
) -> String {
let Some(&command) = path.last() else {
return String::new();
};
let visible_flags = VisibleFlag::collect(path);
let (grouped_keys, grouped_rows) = grouped_rows(path, &visible_flags, style);
let mut output = String::new();
let description = style.text(command.about, command.description);
if let Some(description) = description.filter(|description| !description.is_empty()) {
output.push_str(description);
output.push_str("\n\n");
}
output.push_str("Usage: ");
output.push_str(&render_usage(path));
output.push('\n');
let ungrouped_args = command
.args
.iter()
.copied()
.filter(|arg| !grouped_keys.contains(&arg.key))
.collect::<Vec<_>>();
if !ungrouped_args.is_empty() {
output.push_str("\nArguments:\n");
let rows = ungrouped_args.iter().map(|arg| style.arg_row(arg)).collect::<Vec<_>>();
write_rows(&mut output, &rows, style);
}
if !command.subcommands.is_empty() {
output.push_str("\nCommands:\n");
let mut rows = command
.subcommands
.iter()
.map(|command| {
(display_bytes(command.name.as_bytes()), command.about.unwrap_or("").to_owned())
})
.collect::<Vec<_>>();
if schema_enabled && path.len() == 1 {
rows.push(("schema".to_owned(), "Print machine-readable schema".to_owned()));
}
write_rows(&mut output, &rows, HelpStyle::Short);
}
output.push_str("\nOptions:\n");
let mut rows = visible_flags
.iter()
.filter(|flag| !grouped_keys.contains(&flag.flag.key))
.map(|flag| style.flag_row(flag))
.collect::<Vec<_>>();
rows.extend(command.actions.iter().map(|action| {
(spellings_label(action.shorts, action.longs, None), style.action_help(action))
}));
if schema_enabled {
rows.push((
spellings_label(SCHEMA_ACTION.shorts, SCHEMA_ACTION.longs, None),
style.action_help(&SCHEMA_ACTION),
));
}
write_rows(&mut output, &rows, style);
for (heading, rows) in grouped_rows {
output.push('\n');
output.push_str(heading);
output.push_str(":\n");
write_rows(&mut output, &rows, style);
}
for section in command.help_sections {
output.push('\n');
output.push_str(section.heading);
output.push_str(":\n");
if !section.body.is_empty() {
output.push_str(section.body);
output.push('\n');
}
}
if styling_enabled() { style_headings(&output) } else { output }
}
fn styling_enabled() -> bool {
std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none()
}
fn style_headings(help: &str) -> String {
let mut styled = String::with_capacity(help.len() + 128);
let mut commands = false;
for line in help.split_inclusive('\n') {
let bare = line.strip_suffix('\n').unwrap_or(line);
if let Some(rest) = bare.strip_prefix("Usage:") {
commands = false;
styled.push_str("\x1b[1;4mUsage:\x1b[0m");
styled.push_str(rest);
} else if is_section_heading(bare) {
commands = bare == "Commands:";
styled.push_str("\x1b[1;4m");
styled.push_str(bare);
styled.push_str("\x1b[0m");
} else if commands {
style_command_row(&mut styled, bare);
} else {
style_flag_row(&mut styled, bare);
}
if line.ends_with('\n') {
styled.push('\n');
}
}
styled
}
fn style_command_row(output: &mut String, line: &str) {
let indent = line.len() - line.trim_start().len();
let (prefix, rest) = line.split_at(indent);
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
if end == 0 {
output.push_str(line);
return;
}
output.push_str(prefix);
output.push_str("\x1b[1m");
output.push_str(&rest[..end]);
output.push_str("\x1b[0m");
output.push_str(&rest[end..]);
}
fn style_flag_row(output: &mut String, line: &str) {
let Some(start) = line.find('-') else {
output.push_str(line);
return;
};
if start > 6 || !line[..start].chars().all(char::is_whitespace) {
output.push_str(line);
return;
}
output.push_str(&line[..start]);
let mut rest = &line[start..];
let mut styled_any = false;
loop {
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
let token = &rest[..end];
if token.len() == 1 || !token.starts_with('-') {
break;
}
output.push_str("\x1b[1m");
output.push_str(token);
output.push_str("\x1b[0m");
styled_any = true;
rest = &rest[end..];
let spaces = rest.len() - rest.trim_start().len();
output.push_str(&rest[..spaces]);
rest = &rest[spaces..];
}
if styled_any {
output.push_str(rest);
} else {
output.push_str(&line[start..]);
}
}
fn is_section_heading(line: &str) -> bool {
matches!(line, "Arguments:" | "Commands:" | "Options:")
|| (line.ends_with(':')
&& !line.starts_with(' ')
&& !line.starts_with('\t')
&& !line.contains('`')
&& !line.contains("://"))
}
fn grouped_rows<'a>(
path: &[&'a Command<'a>],
visible_flags: &[VisibleFlag<'a>],
style: HelpStyle,
) -> (Vec<Key>, GroupedHelp<'a>) {
let Some(&selected) = path.last() else {
return (Vec::new(), Vec::new());
};
let selected_scope = path.len() - 1;
let mut grouped_keys = Vec::new();
let mut sections = GroupedHelp::new();
for (scope, command) in path.iter().enumerate().rev() {
for group in command.help_groups.iter().copied() {
if group.heading.is_empty() {
continue;
}
let heading = group.heading;
let mut rows = Vec::new();
if scope == selected_scope {
for arg in selected.args {
if group_contains_arg(group, arg) && !grouped_keys.contains(&arg.key) {
grouped_keys.push(arg.key);
rows.push(style.arg_row(arg));
}
}
}
for flag in visible_flags {
if flag.scope == scope
&& group_contains_flag(group, flag.flag)
&& !grouped_keys.contains(&flag.flag.key)
{
grouped_keys.push(flag.flag.key);
rows.push(style.flag_row(flag));
}
}
if rows.is_empty() {
continue;
}
if let Some((_, existing)) =
sections.iter_mut().find(|(existing, _)| *existing == heading)
{
existing.extend(rows);
} else {
sections.push((heading, rows));
}
}
}
(grouped_keys, sections)
}
fn group_contains_flag(group: &HelpGroup<'_>, flag: &Flag<'_>) -> bool {
group.flags.iter().any(|candidate| std::ptr::eq(*candidate, flag))
}
fn group_contains_arg(group: &HelpGroup<'_>, arg: &Arg<'_>) -> bool {
group.args.iter().any(|candidate| std::ptr::eq(*candidate, arg))
}
fn write_rows(output: &mut String, rows: &[(String, String)], style: HelpStyle) {
if style == HelpStyle::Long {
for (index, (label, help)) in rows.iter().enumerate() {
let label = aligned_label(label);
let _ = writeln!(output, " {label}");
if !help.is_empty() {
write_indented(output, help, 10);
}
if index + 1 != rows.len() {
output.push('\n');
}
}
return;
}
let labels = rows.iter().map(|(label, _)| aligned_label(label)).collect::<Vec<_>>();
let width = labels.iter().map(|label| label.chars().count()).max().unwrap_or(0);
for ((_, help), label) in rows.iter().zip(labels) {
if help.is_empty() {
let _ = writeln!(output, " {label}");
} else {
let _ = writeln!(output, " {label:<width$} {help}");
}
}
}
fn aligned_label(label: &str) -> String {
if label.starts_with("--") { format!(" {label}") } else { label.to_owned() }
}
fn write_indented(output: &mut String, text: &str, indent: usize) {
let padding = " ".repeat(indent);
for line in text.lines() {
if line.is_empty() {
output.push('\n');
} else {
let _ = writeln!(output, "{padding}{line}");
}
}
}
fn metadata_help(
help: Option<&str>,
values: &[&str],
default: Option<&str>,
style: HelpStyle,
) -> String {
let mut output = help.unwrap_or("").to_owned();
match style {
HelpStyle::Short => {
append_inline_values(&mut output, values);
append_inline_default(&mut output, default);
}
HelpStyle::Long => {
if !values.is_empty() {
if !output.is_empty() {
output.push_str("\n\n");
}
output.push_str("Possible values:\n");
for value in values {
output.push_str("- ");
output.push_str(&display_bytes(value.as_bytes()));
output.push('\n');
}
output.pop();
}
if let Some(default) = default {
if !output.is_empty() {
output.push_str("\n\n");
}
output.push_str("[default: ");
output.push_str(&display_bytes(default.as_bytes()));
output.push(']');
}
}
}
output
}
fn append_inline_values(help: &mut String, values: &[&str]) {
if values.is_empty() {
return;
}
if !help.is_empty() {
help.push(' ');
}
help.push_str("[possible values: ");
for (index, value) in values.iter().enumerate() {
if index > 0 {
help.push_str(", ");
}
help.push_str(&display_bytes(value.as_bytes()));
}
help.push(']');
}
fn append_inline_default(help: &mut String, default: Option<&str>) {
let Some(default) = default else {
return;
};
if !help.is_empty() {
help.push(' ');
}
help.push_str("[default: ");
help.push_str(&display_bytes(default.as_bytes()));
help.push(']');
}
fn spellings_label(shorts: &[u8], longs: &[&str], value_name: Option<&str>) -> String {
let mut label = String::new();
for (index, short) in shorts.iter().enumerate() {
if index > 0 {
label.push_str(", ");
}
label.push('-');
label.push(char::from(*short));
}
for long in longs {
if !label.is_empty() {
label.push_str(", ");
}
label.push_str("--");
label.push_str(long);
}
if let Some(name) = value_name {
label.push_str(" <");
label.push_str(&metavar(name));
label.push('>');
}
label
}
pub(crate) fn render_usage(path: &[&Command<'_>]) -> String {
render_usage_inner(path, true)
}
pub(crate) fn render_required_usage(path: &[&Command<'_>]) -> String {
render_usage_inner(path, false)
}
fn render_usage_inner(path: &[&Command<'_>], include_options: bool) -> String {
let Some(&command) = path.last() else {
return String::new();
};
let mut usage = String::new();
for (index, command) in path.iter().enumerate() {
if !usage.is_empty() {
usage.push(' ');
}
usage.push_str(&display_bytes(command.name.as_bytes()));
if include_options && index + 1 == path.len() {
usage.push_str(" [OPTIONS]");
}
for flag in command.flags.iter().filter(|flag| flag.required) {
usage.push(' ');
usage.push_str(&required_flag_usage(flag));
}
if index + 1 != path.len() {
for arg in command.args.iter().filter(|arg| arg.required) {
usage.push(' ');
usage.push_str(&arg_usage(arg));
}
}
}
for arg in command.args {
usage.push(' ');
usage.push_str(&arg_usage(arg));
}
if !command.subcommands.is_empty() {
usage.push_str(" <COMMAND>");
}
usage
}
pub(crate) fn missing_required_labels(path: &[&Command<'_>], supplied: &[Key]) -> Vec<String> {
let mut missing = Vec::new();
for command in path {
for flag in command.flags.iter().filter(|flag| flag.required) {
if !supplied.contains(&flag.key) {
missing.push(required_flag_usage(flag));
}
}
for arg in command.args.iter().filter(|arg| arg.required) {
if !supplied.contains(&arg.key) {
missing.push(arg_usage(arg));
}
}
}
missing
}
pub(crate) fn missing_required_label(path: &[&Command<'_>], diagnostic: &str) -> Option<String> {
if diagnostic.starts_with('<') && diagnostic.ends_with('>') {
return Some(diagnostic.to_owned());
}
for command in path.iter().rev() {
if let Some(flag) = command.flags.iter().find(|flag| flag.diagnostic == diagnostic) {
return Some(required_flag_usage(flag));
}
if let Some(arg) = command.args.iter().find(|arg| arg.name == diagnostic) {
return Some(arg_usage(arg));
}
}
None
}
fn required_flag_usage(flag: &Flag<'_>) -> String {
let mut usage = flag.longs.first().map_or_else(
|| {
flag.shorts.first().map_or_else(
|| flag.name.to_owned(),
|short| {
let short = char::from(*short);
format!("-{short}")
},
)
},
|long| format!("--{long}"),
);
if flag.takes_value {
usage.push_str(" <");
usage.push_str(&metavar(flag.name));
usage.push('>');
}
usage
}
fn arg_usage(arg: &Arg<'_>) -> String {
let name = metavar(arg.name);
let mut usage = if arg.required { format!("<{name}>") } else { format!("[{name}]") };
if arg.variadic {
usage.push_str("...");
}
usage
}
fn metavar(name: &str) -> String {
name.replace('-', "_").to_ascii_uppercase()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::__private::ActionKind;
static VERBOSE: Flag<'static> = Flag {
key: 1,
name: "verbose",
help: Some("Enable verbose output"),
longs: &["verbose"],
shorts: b"v",
..Flag::BOOL
};
static OUTPUT: Flag<'static> = Flag {
key: 2,
name: "destination",
help: Some("Write to this path"),
longs: &["destination"],
required: true,
..Flag::VALUE
};
static PROFILE: Flag<'static> = Flag {
key: 6,
name: "profile",
help: Some("Select a profile"),
longs: &["profile"],
..Flag::VALUE
};
static INPUT: Arg<'static> =
Arg { key: 3, name: "input", help: Some("Input file"), ..Arg::REQUIRED };
static REST: Arg<'static> =
Arg { key: 4, name: "rest", required: false, variadic: true, ..Arg::REQUIRED };
static GET: Command<'static> =
Command { name: "get", about: Some("Read one value"), ..Command::EMPTY };
static CONFIG: Command<'static> = Command {
name: "config",
about: Some("Manage configuration"),
flags: &[&VERBOSE, &OUTPUT, &PROFILE],
args: &[&INPUT, &REST],
subcommands: &[&GET],
key: 5,
..Command::EMPTY
};
static ROOT: Command<'static> = Command {
name: "tool",
about: Some("Example tool"),
subcommands: &[&CONFIG],
..Command::EMPTY
};
#[test]
fn command_and_value_metadata_cannot_inject_terminal_controls() {
let token = Flag {
key: 99,
name: "token",
help: None,
longs: &["token"],
accepted_values: &["safe", "bad\n\u{1b}[31m"],
..Flag::VALUE
};
let flags = [&token];
let command = Command { name: "tool\n\u{1b}[31m", flags: &flags, ..Command::EMPTY };
let help = render(&[&command]);
assert!(!help.contains("tool\n\u{1b}"));
assert!(!help.contains("bad\n\u{1b}"));
assert!(!help.contains('\u{1b}'));
assert!(help.contains(r"bad\n"));
assert!(help.contains(r"tool\n"));
}
#[test]
fn renders_scope_aware_aligned_help() {
snapbox::Assert::new().action_env("SNAPSHOTS").eq(
render(&[&ROOT, &CONFIG]),
snapbox::str![[r#"
Manage configuration
Usage: tool config [OPTIONS] --destination <DESTINATION> <INPUT> [REST]... <COMMAND>
Arguments:
<INPUT> Input file
[REST]...
Commands:
get Read one value
Options:
-v, --verbose Enable verbose output
--destination <DESTINATION> Write to this path
--profile <PROFILE> Select a profile
-h, --help Print help (see more with '--help')
"#]],
);
}
#[test]
fn styling_emphasizes_headings_commands_and_flags() {
let styled = style_headings(
"Usage: tool\n\nCommands:\n serve Start server\n\nLogging:\n -v, --verbose Verbose\n --level <LEVEL> Log level\n -v Errors\n",
);
assert!(styled.contains("\x1b[1;4mUsage:\x1b[0m tool"));
assert!(styled.contains("\x1b[1;4mCommands:\x1b[0m"));
assert!(styled.contains(" \x1b[1mserve\x1b[0m Start server"));
assert!(styled.contains("\x1b[1;4mLogging:\x1b[0m"));
assert!(styled.contains(" \x1b[1m-v,\x1b[0m \x1b[1m--verbose\x1b[0m Verbose"));
assert!(styled.contains(" \x1b[1m--level\x1b[0m <LEVEL> Log level"));
assert!(styled.contains(" -v Errors"));
}
#[test]
fn descendant_help_includes_visible_globals_with_parser_shadowing() {
static ROOT_SCOPE: Flag<'static> = Flag {
key: 10,
name: "root-scope",
help: Some("Root scope"),
longs: &["scope", "root-scope"],
shorts: b"s",
global: true,
..Flag::BOOL
};
static ROOT_PROFILE: Flag<'static> = Flag {
key: 11,
name: "profile",
help: Some("Required profile"),
longs: &["profile"],
shorts: b"p",
global: true,
required: true,
..Flag::VALUE
};
static ROOT_VERSION: Flag<'static> = Flag {
key: 12,
name: "root-version",
help: Some("Root version selector"),
longs: &["version", "root-version"],
global: true,
..Flag::BOOL
};
static MID_SCOPE: Flag<'static> = Flag {
key: 13,
name: "mid-scope",
help: Some("Mid scope"),
longs: &["scope", "mid-scope"],
shorts: b"m",
global: true,
..Flag::BOOL
};
static LOCAL_SCOPE: Flag<'static> = Flag {
key: 14,
name: "scope",
help: Some("Leaf scope"),
longs: &["scope"],
shorts: b"l",
..Flag::BOOL
};
static VERSION: Action<'static> = Action {
name: "version",
diagnostic: "--version",
help: "Print version",
longs: &["version"],
shorts: b"V",
kind: ActionKind::Version { short: "1", long: "1" },
};
static LEAF: Command<'static> = Command {
name: "leaf",
actions: &[&HELP_ACTION, &VERSION],
flags: &[&LOCAL_SCOPE],
..Command::EMPTY
};
static MID: Command<'static> =
Command { name: "mid", flags: &[&MID_SCOPE], subcommands: &[&LEAF], ..Command::EMPTY };
static GLOBAL_ROOT: Command<'static> = Command {
name: "tool",
flags: &[&ROOT_SCOPE, &ROOT_PROFILE, &ROOT_VERSION],
subcommands: &[&MID],
..Command::EMPTY
};
snapbox::Assert::new().action_env("SNAPSHOTS").eq(
render(&[&GLOBAL_ROOT, &MID, &LEAF]),
snapbox::str![[r#"
Usage: tool --profile <PROFILE> mid leaf [OPTIONS]
Options:
-l, --scope Leaf scope
-m, --mid-scope Mid scope
-s, --root-scope Root scope
-p, --profile <PROFILE> Required profile
--root-version Root version selector
-h, --help Print help (see more with '--help')
-V, --version Print version
"#]],
);
}
#[test]
fn descendant_usage_keeps_required_ancestor_flags_at_their_declaring_scope() {
static ROOT_TOKEN: Flag<'static> = Flag {
key: 20,
name: "root-token",
help: Some("Root token"),
longs: &["token"],
global: true,
required: true,
..Flag::VALUE
};
static ROOT_CONFIG: Flag<'static> = Flag {
key: 21,
name: "config",
help: Some("Root config"),
longs: &["config"],
required: true,
..Flag::VALUE
};
static LOCAL_TOKEN: Flag<'static> = Flag {
key: 22,
name: "token",
help: Some("Leaf token"),
longs: &["token"],
..Flag::VALUE
};
static LEAF: Command<'static> =
Command { name: "leaf", flags: &[&LOCAL_TOKEN], ..Command::EMPTY };
static ROOT: Command<'static> = Command {
name: "tool",
flags: &[&ROOT_TOKEN, &ROOT_CONFIG],
subcommands: &[&LEAF],
..Command::EMPTY
};
snapbox::Assert::new().action_env("SNAPSHOTS").eq(
render(&[&ROOT, &LEAF]),
snapbox::str![[r#"
Usage: tool --token <ROOT_TOKEN> --config <CONFIG> leaf [OPTIONS]
Options:
--token <TOKEN> Leaf token
-h, --help Print help (see more with '--help')
"#]],
);
}
#[test]
fn reused_global_mount_is_listed_only_for_the_nearest_scope() {
static SHARED: Flag<'static> = Flag {
key: 30,
name: "shared",
help: Some("Shared setting"),
longs: &["shared"],
global: true,
..Flag::VALUE
};
static LEAF: Command<'static> =
Command { name: "leaf", flags: &[&SHARED], ..Command::EMPTY };
static ROOT: Command<'static> =
Command { name: "tool", flags: &[&SHARED], subcommands: &[&LEAF], ..Command::EMPTY };
snapbox::Assert::new().action_env("SNAPSHOTS").eq(
render(&[&ROOT, &LEAF]),
snapbox::str![[r#"
Usage: tool leaf [OPTIONS]
Options:
--shared <SHARED> Shared setting
-h, --help Print help (see more with '--help')
"#]],
);
}
}