use std::io::Write;
use std::io::{self};
use crate::codegen::collapse_lines;
use crate::codegen::combined_args;
use crate::codegen::command_pieces;
use crate::codegen::command_type_prefix;
use crate::codegen::ensure_unique_command_prefixes;
use crate::codegen::inherited_globals;
use crate::codegen::is_grouped_repeated;
use crate::codegen::is_required;
use crate::codegen::lower_join;
use crate::codegen::option_token;
use crate::codegen::output_schema;
use crate::codegen::pascal_case;
use crate::codegen::quote_double;
use crate::codegen::safe_identifier;
use crate::generate::Generator;
use crate::generate::OutputContractGeneration;
use crate::model::ArgKind;
use crate::model::ArgSpec;
use crate::model::CliSpec;
use crate::model::CommandSpec;
use crate::model::OutputEncoding;
use crate::model::OutputMode;
use crate::model::ValueType;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RustOptions {
pub module_name: Option<String>,
pub output_contracts: OutputContractGeneration,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Rust {
options: RustOptions,
}
impl Rust {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_options(options: RustOptions) -> Self {
Self { options }
}
#[must_use]
pub fn module_name(mut self, module_name: impl Into<String>) -> Self {
self.options.module_name = Some(module_name.into());
self
}
#[must_use]
pub fn output_contracts(mut self) -> Self {
self.options.output_contracts = OutputContractGeneration::Emit;
self
}
#[must_use]
pub fn without_output_contracts(mut self) -> Self {
self.options.output_contracts = OutputContractGeneration::Omit;
self
}
}
impl Generator for Rust {
fn file_name(&self, bin_name: &str) -> String {
let stem = self
.options
.module_name
.as_deref()
.map(snake_case)
.unwrap_or_else(|| snake_case(bin_name));
format!("{stem}.rs")
}
fn generate(&self, spec: &CliSpec, buf: &mut dyn Write) -> io::Result<()> {
ensure_unique_command_prefixes(spec)?;
let mut output = String::new();
render_module(spec, &self.options, &mut output);
buf.write_all(output.as_bytes())
}
}
fn render_module(spec: &CliSpec, options: &RustOptions, output: &mut String) {
output.push_str("// Generated by clap_types. Do not edit by hand.\n");
output.push_str("#![allow(dead_code)]\n\n");
output.push_str(&format!(
"pub const PROGRAM: &str = {};\n\n",
quote_double(&spec.bin_name)
));
render_invocation(output);
render_helpers(output);
if options.output_contracts.is_enabled() {
render_output_contracts(spec, output);
}
let mut path = Vec::<String>::new();
let inherited: Vec<&ArgSpec> = Vec::new();
render_command_tree(&spec.root, &mut path, &inherited, output);
if output.ends_with("\n\n") {
output.pop();
}
}
fn render_invocation(output: &mut String) {
output.push_str(
r#"#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandInvocation {
pub program: String,
pub args: Vec<String>,
}
impl CommandInvocation {
#[must_use]
pub fn argv(&self) -> Vec<String> {
let mut argv = Vec::with_capacity(self.args.len() + 1);
argv.push(self.program.clone());
argv.extend(self.args.clone());
argv
}
#[must_use]
pub fn command(&self) -> std::process::Command {
let mut command = std::process::Command::new(&self.program);
command.args(&self.args);
command
}
pub fn output(&self) -> std::io::Result<std::process::Output> {
self.command().output()
}
}
"#,
);
}
fn render_helpers(output: &mut String) {
output.push_str(
r#"trait CliArgValue {
fn to_cli_arg(&self) -> String;
}
impl CliArgValue for String {
fn to_cli_arg(&self) -> String {
self.clone()
}
}
impl CliArgValue for bool {
fn to_cli_arg(&self) -> String {
if *self {
"true".to_owned()
} else {
"false".to_owned()
}
}
}
impl CliArgValue for u8 {
fn to_cli_arg(&self) -> String {
self.to_string()
}
}
impl CliArgValue for i32 {
fn to_cli_arg(&self) -> String {
self.to_string()
}
}
impl CliArgValue for i128 {
fn to_cli_arg(&self) -> String {
self.to_string()
}
}
impl CliArgValue for f64 {
fn to_cli_arg(&self) -> String {
self.to_string()
}
}
impl CliArgValue for std::path::PathBuf {
fn to_cli_arg(&self) -> String {
self.to_string_lossy().into_owned()
}
}
impl CliArgValue for std::ffi::OsString {
fn to_cli_arg(&self) -> String {
self.to_string_lossy().into_owned()
}
}
fn push_one<T: CliArgValue>(argv: &mut Vec<String>, value: &T) {
argv.push(value.to_cli_arg());
}
fn push_values<T: CliArgValue>(argv: &mut Vec<String>, values: &[T]) {
for value in values {
push_one(argv, value);
}
}
fn push_option<T: CliArgValue>(argv: &mut Vec<String>, option: &str, value: &T) {
argv.push(option.to_owned());
push_one(argv, value);
}
fn push_option_values<T: CliArgValue>(argv: &mut Vec<String>, option: &str, values: &[T]) {
argv.push(option.to_owned());
push_values(argv, values);
}
fn push_repeated_option<T: CliArgValue>(argv: &mut Vec<String>, option: &str, values: &[T]) {
for value in values {
push_option(argv, option, value);
}
}
fn push_grouped_repeated_option<T: CliArgValue>(
argv: &mut Vec<String>,
option: &str,
groups: &[Vec<T>],
) {
for group in groups {
push_option_values(argv, option, group);
}
}
"#,
);
}
fn render_output_contracts(spec: &CliSpec, output: &mut String) {
output.push_str(
r#"#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputEncoding {
Json,
JsonLines,
Text,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Buffered,
Streaming,
Interactive,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputContract {
pub command_path: &'static [&'static str],
pub encoding: OutputEncoding,
pub mode: OutputMode,
pub type_name: &'static str,
pub schema: Option<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsedOutput {
Json(String),
JsonLines(Vec<String>),
Text(String),
Interactive(String),
}
"#,
);
if spec.outputs.is_empty() {
output.push_str("pub const OUTPUT_CONTRACTS: &[OutputContract] = &[];\n\n");
} else {
output.push_str("pub const OUTPUT_CONTRACTS: &[OutputContract] = &[\n");
for contract in &spec.outputs {
output.push_str(" OutputContract {\n");
output.push_str(&format!(
" command_path: &[{}],\n",
contract
.command_path
.iter()
.map(|piece| quote_double(piece))
.collect::<Vec<_>>()
.join(", ")
));
output.push_str(&format!(
" encoding: OutputEncoding::{},\n",
rust_output_encoding(contract.encoding)
));
output.push_str(&format!(
" mode: OutputMode::{},\n",
rust_output_mode(contract.mode)
));
output.push_str(&format!(
" type_name: {},\n",
quote_double(&contract.type_name)
));
match contract.schema.as_ref().map(output_schema) {
Some(schema) => output.push_str(&format!(
" schema: Some({}),\n",
quote_double(schema)
)),
None => output.push_str(" schema: None,\n"),
}
output.push_str(" },\n");
}
output.push_str(
r#"];
"#,
);
}
output.push_str(
r#"pub fn parse_output(contract: &OutputContract, stdout: impl Into<String>) -> ParsedOutput {
let stdout = stdout.into();
if matches!(contract.mode, OutputMode::Interactive) {
return ParsedOutput::Interactive(stdout);
}
match contract.encoding {
OutputEncoding::Json => ParsedOutput::Json(stdout),
OutputEncoding::JsonLines => ParsedOutput::JsonLines(
stdout
.lines()
.filter(|line| !line.is_empty())
.map(str::to_owned)
.collect(),
),
OutputEncoding::Text => ParsedOutput::Text(stdout),
}
}
"#,
);
}
fn render_command_tree(
command: &CommandSpec,
path: &mut Vec<String>,
inherited: &[&ArgSpec],
output: &mut String,
) {
render_command(command, path, inherited, output);
let next_inherited = inherited_globals(inherited, command);
for subcommand in &command.subcommands {
path.push(subcommand.name.clone());
render_command_tree(subcommand, path, &next_inherited, output);
path.pop();
}
}
fn render_command(
command: &CommandSpec,
path: &[String],
inherited: &[&ArgSpec],
output: &mut String,
) {
let type_prefix = command_type_prefix(command, path);
let args_name = format!("{type_prefix}Args");
let fn_prefix = command_function_prefix(command, path);
let build_fn = format!("build_{fn_prefix}_command");
let command_fn = format!("{fn_prefix}_command");
let command_with_program_fn = format!("{fn_prefix}_command_with_program");
let all_args = combined_args(inherited, command);
let has_default = !all_args.iter().copied().any(is_required);
render_enums(&type_prefix, &all_args, output);
render_struct(command, &args_name, &all_args, has_default, output);
output.push('\n');
render_build_function(command, path, inherited, &args_name, &build_fn, output);
output.push('\n');
output.push_str(&format!(
"pub fn {command_fn}(args: &{args_name}) -> CommandInvocation {{\n"
));
output.push_str(&format!(" {command_with_program_fn}(args, PROGRAM)\n"));
output.push_str("}\n\n");
let with_program_signature = format!(
"pub fn {command_with_program_fn}(args: &{args_name}, program: impl Into<String>) -> CommandInvocation {{"
);
if with_program_signature.len() <= 100 {
output.push_str(&with_program_signature);
output.push('\n');
} else {
output.push_str(&format!(
"pub fn {command_with_program_fn}(\n args: &{args_name},\n program: impl Into<String>,\n) -> CommandInvocation {{\n"
));
}
output.push_str(" CommandInvocation {\n");
output.push_str(" program: program.into(),\n");
output.push_str(&format!(" args: {build_fn}(args),\n"));
output.push_str(" }\n");
output.push_str("}\n\n");
}
fn render_enums(type_prefix: &str, args: &[&ArgSpec], output: &mut String) {
for &arg in args {
if arg.possible_values.is_empty() {
continue;
}
let enum_name = enum_name(type_prefix, arg);
output.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n");
output.push_str(&format!("pub enum {enum_name} {{\n"));
for value in &arg.possible_values {
output.push_str(&format!(" {},\n", enum_variant(&value.name)));
}
output.push_str("}\n\n");
output.push_str(&format!("impl {enum_name} {{\n"));
output.push_str(" pub const fn as_str(self) -> &'static str {\n");
output.push_str(" match self {\n");
for value in &arg.possible_values {
output.push_str(&format!(
" Self::{} => {},\n",
enum_variant(&value.name),
quote_double(&value.name)
));
}
output.push_str(" }\n");
output.push_str(" }\n");
output.push_str("}\n\n");
output.push_str(&format!("impl CliArgValue for {enum_name} {{\n"));
output.push_str(" fn to_cli_arg(&self) -> String {\n");
output.push_str(" self.as_str().to_owned()\n");
output.push_str(" }\n");
output.push_str("}\n\n");
}
}
fn render_struct(
command: &CommandSpec,
args_name: &str,
args: &[&ArgSpec],
has_default: bool,
output: &mut String,
) {
let default = if has_default { ", Default" } else { "" };
if let Some(doc) = command.about.as_deref().or(command.long_about.as_deref()) {
output.push_str(&format!("/// {}\n", rust_doc(doc)));
}
output.push_str(&format!("#[derive(Debug, Clone, PartialEq{default})]\n"));
output.push_str(&format!("pub struct {args_name} {{\n"));
if args.is_empty() {
output.push_str("}\n");
return;
}
for arg in sorted_struct_args(args) {
for comment in field_comments(arg) {
output.push_str(&format!(" /// {comment}\n"));
}
output.push_str(&format!(
" pub {}: {},\n",
property_name(arg),
rust_type(args_name.trim_end_matches("Args"), arg)
));
}
output.push_str("}\n");
}
fn sorted_struct_args<'a>(args: &[&'a ArgSpec]) -> Vec<&'a ArgSpec> {
let mut sorted = args.to_vec();
sorted.sort_by_key(|arg| !is_required(arg));
sorted
}
fn render_build_function(
command: &CommandSpec,
path: &[String],
inherited: &[&ArgSpec],
args_name: &str,
build_fn: &str,
output: &mut String,
) {
output.push_str(&format!(
"pub fn {build_fn}(args: &{args_name}) -> Vec<String> {{\n"
));
if inherited.is_empty() && path.is_empty() && command.args.is_empty() {
output.push_str(" let _ = args;\n");
output.push_str(" Vec::new()\n");
output.push_str("}\n");
return;
}
output.push_str(" let mut argv = Vec::<String>::new();\n");
for &arg in inherited {
render_arg_builder(arg, output);
}
for token in path {
output.push_str(&format!(
" argv.push({}.to_owned());\n",
quote_double(token)
));
}
for arg in &command.args {
if !inherited.iter().any(|existing| existing.id == arg.id) {
render_arg_builder(arg, output);
}
}
output.push_str(" argv\n");
output.push_str("}\n");
}
fn render_arg_builder(arg: &ArgSpec, output: &mut String) {
let prop = property_name(arg);
let option = option_token(arg);
let accessor = format!("args.{prop}");
match arg.kind {
ArgKind::FlagTrue => {
if let Some(option) = option {
output.push_str(&format!(" if {accessor} {{\n"));
output.push_str(&format!(
" argv.push({}.to_owned());\n",
quote_double(&option)
));
output.push_str(" }\n");
}
}
ArgKind::FlagFalse => {
if let Some(option) = option {
output.push_str(&format!(" if {accessor} == Some(false) {{\n"));
output.push_str(&format!(
" argv.push({}.to_owned());\n",
quote_double(&option)
));
output.push_str(" }\n");
}
}
ArgKind::Counter => {
if let Some(option) = option {
output.push_str(&format!(" for _ in 0..{accessor} {{\n"));
output.push_str(&format!(
" argv.push({}.to_owned());\n",
quote_double(&option)
));
output.push_str(" }\n");
}
}
ArgKind::Option => {
if let Some(option) = option {
render_value_arg_builder(arg, &accessor, &option, true, output);
}
}
ArgKind::Positional => {
render_value_arg_builder(arg, &accessor, "", false, output);
}
}
}
fn render_value_arg_builder(
arg: &ArgSpec,
accessor: &str,
option: &str,
is_option: bool,
output: &mut String,
) {
let option = quote_double(option);
if is_grouped_repeated(arg) {
if is_option {
output.push_str(&format!(
" push_grouped_repeated_option(&mut argv, {option}, &{accessor});\n"
));
} else {
output.push_str(&format!(
" for group in &{accessor} {{ push_values(&mut argv, group); }}\n"
));
}
} else if arg.value.repeated || arg.value.arity.allows_multiple() {
if is_option {
if arg.value.repeated {
output.push_str(&format!(
" push_repeated_option(&mut argv, {option}, &{accessor});\n"
));
} else {
output.push_str(&format!(
" if !{accessor}.is_empty() {{ push_option_values(&mut argv, {option}, &{accessor}); }}\n"
));
}
} else {
output.push_str(&format!(" push_values(&mut argv, &{accessor});\n"));
}
} else if is_required(arg) {
if is_option {
output.push_str(&format!(
" push_option(&mut argv, {option}, &{accessor});\n"
));
} else {
output.push_str(&format!(" push_one(&mut argv, &{accessor});\n"));
}
} else if is_option {
output.push_str(&format!(" if let Some(value) = &{accessor} {{\n"));
output.push_str(&format!(
" push_option(&mut argv, {option}, value);\n"
));
output.push_str(" }\n");
} else {
output.push_str(&format!(" if let Some(value) = &{accessor} {{\n"));
output.push_str(" push_one(&mut argv, value);\n");
output.push_str(" }\n");
}
}
fn rust_type(type_prefix: &str, arg: &ArgSpec) -> String {
let scalar = rust_scalar_type(type_prefix, arg);
match arg.kind {
ArgKind::FlagTrue => "bool".to_owned(),
ArgKind::FlagFalse => "Option<bool>".to_owned(),
ArgKind::Counter => "u8".to_owned(),
ArgKind::Option | ArgKind::Positional => {
if is_grouped_repeated(arg) {
format!("Vec<Vec<{scalar}>>")
} else if arg.value.repeated || arg.value.arity.allows_multiple() {
format!("Vec<{scalar}>")
} else if is_required(arg) {
scalar
} else {
format!("Option<{scalar}>")
}
}
}
}
fn rust_scalar_type(type_prefix: &str, arg: &ArgSpec) -> String {
if !arg.possible_values.is_empty() {
return enum_name(type_prefix, arg);
}
match arg.kind {
ArgKind::FlagTrue | ArgKind::FlagFalse => "bool".to_owned(),
ArgKind::Counter => "u8".to_owned(),
ArgKind::Option | ArgKind::Positional => match arg.value.ty {
ValueType::Bool => "bool".to_owned(),
ValueType::Integer => "i32".to_owned(),
ValueType::BigInteger => "i128".to_owned(),
ValueType::Float => "f64".to_owned(),
ValueType::Path => "std::path::PathBuf".to_owned(),
ValueType::OsString => "std::ffi::OsString".to_owned(),
ValueType::Unknown | ValueType::String => "String".to_owned(),
},
}
}
fn command_function_prefix(command: &CommandSpec, path: &[String]) -> String {
let joined = command_pieces(command, path)
.iter()
.map(|piece| snake_case(piece))
.collect::<Vec<_>>()
.join("_");
safe_identifier(&joined, is_reserved)
}
fn property_name(arg: &ArgSpec) -> String {
safe_identifier(&snake_case(&arg.id), is_reserved)
}
fn enum_name(type_prefix: &str, arg: &ArgSpec) -> String {
format!("{type_prefix}{}", pascal_case(&arg.id))
}
fn enum_variant(value: &str) -> String {
safe_identifier(&pascal_case(value), is_reserved)
}
fn field_comments(arg: &ArgSpec) -> Vec<String> {
let mut comments = Vec::new();
if let Some(help) = arg.help.as_deref().or(arg.long_help.as_deref()) {
comments.push(rust_doc(help));
}
if !arg.defaults.is_empty() {
comments.push(format!("Clap default: {}.", arg.defaults.join(", ")));
}
comments
}
fn rust_doc(input: &str) -> String {
collapse_lines(input).replace("*/", "* /")
}
fn snake_case(input: &str) -> String {
lower_join(input, "_")
}
fn rust_output_encoding(encoding: OutputEncoding) -> &'static str {
match encoding {
OutputEncoding::Json => "Json",
OutputEncoding::JsonLines => "JsonLines",
OutputEncoding::Text => "Text",
}
}
fn rust_output_mode(mode: OutputMode) -> &'static str {
match mode {
OutputMode::Buffered => "Buffered",
OutputMode::Streaming => "Streaming",
OutputMode::Interactive => "Interactive",
}
}
fn is_reserved(identifier: &str) -> bool {
RESERVED.contains(&identifier)
}
const RESERVED: &[&str] = &[
"Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue",
"crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl",
"in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
"return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof",
"unsafe", "unsized", "use", "virtual", "where", "while", "yield",
];
#[cfg(test)]
mod tests {
use std::io::Write;
use clap::Arg;
use clap::ArgAction;
use clap::Command;
use clap::value_parser;
use crate::Generator;
use crate::Rust;
use crate::generate;
#[test]
fn generates_typed_rust_builders() {
let cmd = Command::new("demo-tool")
.arg(Arg::new("verbose").short('v').action(ArgAction::Count))
.subcommand(
Command::new("run")
.arg(Arg::new("target").required(true))
.arg(Arg::new("mode").long("mode").value_parser(["fast", "slow"]))
.arg(
Arg::new("threads")
.long("threads")
.value_parser(value_parser!(u16)),
),
);
let mut output = Vec::<u8>::new();
generate(Rust::new(), &cmd, "demo-tool", &mut output).expect("rust generation works");
let output = String::from_utf8(output).expect("rust is utf-8");
assert!(output.contains("pub const PROGRAM: &str = \"demo-tool\";"));
assert!(output.contains("pub struct RunArgs"));
assert!(output.contains("pub enum RunMode"));
assert!(output.contains("pub target: String,"));
assert!(output.contains("pub threads: Option<i32>,"));
assert!(output.contains("argv.push(\"run\".to_owned());"));
assert!(output.contains("push_option(&mut argv, \"--threads\", value);"));
}
#[test]
#[ignore = "shells out to rustc which is not available in Buck CI"]
fn generated_rust_compiles() {
let cmd = Command::new("demo-tool").subcommand(
Command::new("run")
.arg(Arg::new("target").required(true))
.arg(Arg::new("tag").long("tag").action(ArgAction::Append)),
);
let mut output = Vec::<u8>::new();
generate(Rust::new(), &cmd, "demo-tool", &mut output).expect("rust generation works");
let source = String::from_utf8(output).expect("rust is utf-8");
let dir = std::env::temp_dir().join(format!(
"clap_types_rust_compile_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time")
.as_nanos()
));
std::fs::create_dir_all(&dir).expect("create temp dir");
let source_path = dir.join("bindings.rs");
let lib_path = dir.join("bindings.rlib");
std::fs::File::create(&source_path)
.expect("create bindings.rs")
.write_all(source.as_bytes())
.expect("write bindings.rs");
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
let status = std::process::Command::new(rustc)
.args([
"--edition=2021",
"--crate-type=lib",
source_path.to_str().expect("utf-8 source path"),
"-o",
lib_path.to_str().expect("utf-8 lib path"),
])
.status()
.expect("run rustc");
assert!(status.success(), "generated Rust did not compile");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn variadic_positionals_emit_required_vecs_in_order() {
let cmd = Command::new("demo-tool").subcommand(
Command::new("copy")
.arg(Arg::new("source").required(true))
.arg(
Arg::new("mode")
.long("mode")
.value_parser(["fast", "safe"])
.action(ArgAction::Set),
)
.arg(Arg::new("paths").num_args(1..).required(true)),
);
let mut output = Vec::<u8>::new();
generate(Rust::new(), &cmd, "demo-tool", &mut output).expect("rust generation works");
let output = String::from_utf8(output).expect("rust is utf-8");
assert!(
output.contains("pub paths: Vec<String>,"),
"expected variadic positional Vec type, got:\n{output}"
);
assert!(
output.contains("push_values(&mut argv, &args.paths);"),
"expected variadic positional builder, got:\n{output}"
);
let source_index = output
.find("push_one(&mut argv, &args.source);")
.expect("source positional builder");
let mode_index = output
.find("if let Some(value) = &args.mode")
.expect("mode option builder");
let paths_index = output
.find("push_values(&mut argv, &args.paths);")
.expect("variadic positional builder");
assert!(source_index < mode_index);
assert!(mode_index < paths_index);
}
#[test]
fn emits_output_contracts_when_enabled() {
let spec = crate::CliSpec {
bin_name: "demo".to_owned(),
root: crate::CommandSpec {
name: "demo".to_owned(),
display_name: None,
about: None,
long_about: None,
args: Vec::new(),
subcommands: Vec::new(),
},
outputs: vec![crate::OutputSpec {
command_path: vec!["watch".to_owned()],
encoding: crate::OutputEncoding::JsonLines,
mode: crate::OutputMode::Streaming,
type_name: "WatchEvent".to_owned(),
schema: Some(crate::OutputSchema::JsonSchema(
"{\"type\":\"object\"}".to_owned(),
)),
}],
};
let mut output = Vec::<u8>::new();
Rust::new()
.output_contracts()
.generate(&spec, &mut output)
.expect("rust generation works");
let output = String::from_utf8(output).expect("rust is utf-8");
assert!(output.contains("pub const OUTPUT_CONTRACTS: &[OutputContract]"));
assert!(output.contains("encoding: OutputEncoding::JsonLines"));
assert!(output.contains("pub fn parse_output("));
}
}