use clap::{Args, Parser, Subcommand};
#[derive(Parser, Debug, Clone)]
#[command(
name = "kaze",
about = "Zephyr build system companion.",
version,
propagate_version = true,
after_help = r#"Examples:
# Simple project
kaze -b nucleo_f767zi -r openocd build
kaze flash
# Sysbuild project
kaze flash --list
kaze flash --image app
For more info, see https://gilab.com/byacrates/nishikaze
"#
)]
pub struct Cli {
#[arg(short = 'c', long = "clean")]
pub preclean: bool,
#[arg(short = 'a', long = "all")]
pub all: bool,
#[arg(short = 'p', long = "profile")]
pub profile: Option<String>,
#[arg(short = 'b', long = "board")]
pub board: Option<String>,
#[arg(short = 'r', long = "runner")]
pub runner: Option<String>,
#[arg(long = "project")]
pub project: Option<std::path::PathBuf>,
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, default_value_t = 2)]
pub verbose: u8,
#[arg(short = 'd', long = "dry-run")]
pub dry_run: bool,
#[command(subcommand)]
pub command: Command,
}
impl Cli {
#[must_use]
pub fn parse_args() -> Self {
Self::parse()
}
}
#[derive(Subcommand, Debug, Clone)]
pub enum Command {
#[command(alias = "i")]
Init(InitArgs),
#[command(alias = "bo")]
Boards,
#[command(alias = "rn")]
Runners,
#[command(alias = "p")]
Profiles,
#[command(alias = "c")]
Clean,
#[command(alias = "cf")]
Conf(PhaseArgs),
#[command(alias = "b")]
Build(PhaseArgs),
#[command(alias = "r")]
Run(RunArgs),
#[command(alias = "f")]
Flash(FlashArgs),
#[command(alias = "m")]
Menuconfig(MenuconfigArgs),
#[command(alias = "spdx")]
Bom(BomArgs),
#[command(alias = "ws")]
Workspace(WsArgs),
}
#[derive(Args, Debug, Default, Clone)]
pub struct PhaseArgs {
#[arg(trailing_var_arg = true)]
pub extra: Vec<String>,
}
#[derive(Args, Debug, Default, Clone)]
pub struct SysbuildArgs {
#[arg(short = 'l', long = "list")]
pub list: bool,
#[arg(short = 'i', long = "image")]
pub image: Option<String>,
}
#[derive(Args, Debug, Default, Clone)]
pub struct RunArgs {
#[arg(short = 'n', long = "norebuild")]
pub norebuild: bool,
#[command(flatten)]
pub sys: SysbuildArgs,
#[command(flatten)]
pub phase: PhaseArgs,
}
#[derive(Args, Debug, Default, Clone)]
pub struct FlashArgs {
#[command(flatten)]
pub sys: SysbuildArgs,
#[command(flatten)]
pub phase: PhaseArgs,
}
#[derive(Args, Debug, Default, Clone)]
pub struct MenuconfigArgs {
#[command(flatten)]
pub sys: SysbuildArgs,
}
#[derive(Args, Debug, Default, Clone)]
pub struct BomArgs {
#[command(flatten)]
pub sys: SysbuildArgs,
}
#[derive(Args, Debug, Default, Clone)]
pub struct InitArgs {
#[arg(short = 'f', long = "force")]
pub force: bool,
}
#[derive(Args, Debug, Clone)]
pub struct WsArgs {
#[command(subcommand)]
pub command: WsCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum WsCommand {
#[command(alias = "i")]
Init(WsInitArgs),
#[command(alias = "u")]
Update,
#[command(alias = "e")]
Export,
#[command(alias = "a")]
Apply(WsApplyArgs),
}
#[derive(Args, Debug, Default, Clone)]
pub struct WsInitArgs {
#[arg(short = 'f', long = "force")]
pub force: bool,
}
#[derive(Args, Debug, Default, Clone)]
pub struct WsApplyArgs {
#[arg(short = 'f', long = "force")]
pub force: bool,
#[arg(short = 'n', long = "noupdate")]
pub noupdate: bool,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::*;
#[test]
fn parse_defaults() {
let cli = Cli::try_parse_from(["kaze", "clean"]).expect("parse ok");
assert!(!cli.preclean);
assert!(!cli.all);
assert_eq!(cli.profile, None);
assert_eq!(cli.board, None);
assert_eq!(cli.runner, None);
assert_eq!(cli.verbose, 2);
assert!(matches!(cli.command, Command::Clean));
}
#[test]
fn parse_flags_and_options() {
let cli = Cli::try_parse_from([
"kaze",
"-c",
"-a",
"-p",
"dev",
"-b",
"native_sim",
"-r",
"native",
"-v",
"-v",
"build",
])
.expect("parse ok");
assert!(cli.preclean);
assert!(cli.all);
assert_eq!(cli.profile.as_deref(), Some("dev"));
assert_eq!(cli.board.as_deref(), Some("native_sim"));
assert_eq!(cli.runner.as_deref(), Some("native"));
assert_eq!(cli.verbose, 2);
assert!(matches!(cli.command, Command::Build(_)));
}
#[test]
fn parse_subcommands_with_args() {
let cli = Cli::try_parse_from(["kaze", "run", "-n", "-l", "-i", "app", "--", "-DOPT=1"])
.expect("parse ok");
assert!(matches!(cli.command, Command::Run(_)));
let Command::Run(args) = cli.command else {
return;
};
assert!(args.norebuild);
assert!(args.sys.list);
assert_eq!(args.sys.image.as_deref(), Some("app"));
assert_eq!(args.phase.extra, vec!["-DOPT=1"]);
}
#[test]
fn parse_simple_subcommands() {
let cli_boards = Cli::try_parse_from(["kaze", "boards"]).expect("parse ok");
assert!(matches!(cli_boards.command, Command::Boards));
let cli_runners = Cli::try_parse_from(["kaze", "runners"]).expect("parse ok");
assert!(matches!(cli_runners.command, Command::Runners));
let cli_profiles = Cli::try_parse_from(["kaze", "profiles"]).expect("parse ok");
assert!(matches!(cli_profiles.command, Command::Profiles));
}
#[test]
fn parse_conf_and_flash_with_passthrough() {
let cli_conf = Cli::try_parse_from(["kaze", "conf", "--", "-DOPT=1"]).expect("parse ok");
assert!(matches!(cli_conf.command, Command::Conf(_)));
let Command::Conf(conf_args) = cli_conf.command else {
return;
};
assert_eq!(conf_args.extra, vec!["-DOPT=1"]);
let cli_flash =
Cli::try_parse_from(["kaze", "flash", "-l", "-i", "app"]).expect("parse ok");
assert!(matches!(cli_flash.command, Command::Flash(_)));
let Command::Flash(flash_args) = cli_flash.command else {
return;
};
assert!(flash_args.sys.list);
assert_eq!(flash_args.sys.image.as_deref(), Some("app"));
}
#[test]
fn parse_bom_with_sysbuild_args() {
let cli = Cli::try_parse_from(["kaze", "bom", "-l", "-i", "app"]).expect("parse ok");
assert!(matches!(cli.command, Command::Bom(_)));
let Command::Bom(args) = cli.command else {
return;
};
assert!(args.sys.list);
assert_eq!(args.sys.image.as_deref(), Some("app"));
}
#[test]
fn parse_bom_alias_spdx() {
let cli = Cli::try_parse_from(["kaze", "spdx"]).expect("parse ok");
assert!(matches!(cli.command, Command::Bom(_)));
let Command::Bom(args) = cli.command else {
return;
};
assert!(!args.sys.list);
assert!(args.sys.image.is_none());
}
#[test]
fn parse_menuconfig_with_sysbuild_args() {
let cli = Cli::try_parse_from(["kaze", "menuconfig", "-l", "-i", "app"]).expect("parse ok");
assert!(matches!(cli.command, Command::Menuconfig(_)));
let Command::Menuconfig(args) = cli.command else {
return;
};
assert!(args.sys.list);
assert_eq!(args.sys.image.as_deref(), Some("app"));
}
#[test]
fn parse_menuconfig_alias_m() {
let cli = Cli::try_parse_from(["kaze", "m"]).expect("parse ok");
assert!(matches!(cli.command, Command::Menuconfig(_)));
let Command::Menuconfig(args) = cli.command else {
return;
};
assert!(!args.sys.list);
assert!(args.sys.image.is_none());
}
#[test]
fn parse_aliases_and_defaults() {
let cli_init = Cli::try_parse_from(["kaze", "i"]).expect("parse ok");
assert!(matches!(cli_init.command, Command::Init(_)));
let cli_boards = Cli::try_parse_from(["kaze", "bo"]).expect("parse ok");
assert!(matches!(cli_boards.command, Command::Boards));
let cli_runners = Cli::try_parse_from(["kaze", "rn"]).expect("parse ok");
assert!(matches!(cli_runners.command, Command::Runners));
let cli_profiles = Cli::try_parse_from(["kaze", "p"]).expect("parse ok");
assert!(matches!(cli_profiles.command, Command::Profiles));
let cli_clean = Cli::try_parse_from(["kaze", "c"]).expect("parse ok");
assert!(matches!(cli_clean.command, Command::Clean));
let cli_conf = Cli::try_parse_from(["kaze", "cf"]).expect("parse ok");
assert!(matches!(cli_conf.command, Command::Conf(_)));
let cli_build = Cli::try_parse_from(["kaze", "b"]).expect("parse ok");
assert!(matches!(cli_build.command, Command::Build(_)));
let cli_run = Cli::try_parse_from(["kaze", "r"]).expect("parse ok");
assert!(matches!(&cli_run.command, Command::Run(_)));
if let Command::Run(ref run_args) = cli_run.command {
assert!(!run_args.norebuild);
assert!(!run_args.sys.list);
assert_eq!(run_args.sys.image, None);
assert!(run_args.phase.extra.is_empty());
}
let cli_flash = Cli::try_parse_from(["kaze", "f"]).expect("parse ok");
assert!(matches!(&cli_flash.command, Command::Flash(_)));
if let Command::Flash(ref flash_args) = cli_flash.command {
assert!(!flash_args.sys.list);
assert_eq!(flash_args.sys.image, None);
assert!(flash_args.phase.extra.is_empty());
}
}
#[test]
fn parse_workspace_init_and_alias() {
let cli_full = Cli::try_parse_from(["kaze", "workspace", "init", "-f"]).expect("parse ok");
assert!(matches!(
cli_full.command,
Command::Workspace(WsArgs {
command: WsCommand::Init(WsInitArgs { force: true })
})
));
let cli_alias = Cli::try_parse_from(["kaze", "ws", "i", "-f"]).expect("parse ok");
assert!(matches!(
cli_alias.command,
Command::Workspace(WsArgs {
command: WsCommand::Init(WsInitArgs { force: true })
})
));
}
#[test]
fn parse_workspace_update_and_alias() {
let cli_full = Cli::try_parse_from(["kaze", "ws", "update"]).expect("parse ok");
assert!(matches!(
cli_full.command,
Command::Workspace(WsArgs {
command: WsCommand::Update
})
));
let cli_alias = Cli::try_parse_from(["kaze", "ws", "u"]).expect("parse ok");
assert!(matches!(
cli_alias.command,
Command::Workspace(WsArgs {
command: WsCommand::Update
})
));
}
#[test]
fn parse_workspace_export_and_alias() {
let cli_full = Cli::try_parse_from(["kaze", "ws", "export"]).expect("parse ok");
assert!(matches!(
cli_full.command,
Command::Workspace(WsArgs {
command: WsCommand::Export
})
));
let cli_alias = Cli::try_parse_from(["kaze", "ws", "e"]).expect("parse ok");
assert!(matches!(
cli_alias.command,
Command::Workspace(WsArgs {
command: WsCommand::Export
})
));
}
#[test]
fn parse_workspace_apply_and_alias() {
let cli_full = Cli::try_parse_from(["kaze", "ws", "apply", "-f", "-n"]).expect("parse ok");
assert!(matches!(
cli_full.command,
Command::Workspace(WsArgs {
command: WsCommand::Apply(WsApplyArgs {
force: true,
noupdate: true
})
})
));
let cli_alias = Cli::try_parse_from(["kaze", "ws", "a", "-f", "-n"]).expect("parse ok");
assert!(matches!(
cli_alias.command,
Command::Workspace(WsArgs {
command: WsCommand::Apply(WsApplyArgs {
force: true,
noupdate: true
})
})
));
}
}