@@ -7,7 +7,7 @@ use std::{
77 str:: FromStr ,
88} ;
99
10- use clap:: { ArgGroup , Parser } ;
10+ use clap:: { ArgGroup , Args , Parser , Subcommand } ;
1111use jemallocator:: Jemalloc ;
1212use lading:: {
1313 blackhole,
@@ -125,8 +125,24 @@ impl FromStr for CliKeyValues {
125125 }
126126}
127127
128+ // Parser for subcommand structure
128129#[ derive( Parser ) ]
129130#[ clap( version, about, long_about = None ) ]
131+ struct CliWithSubcommands {
132+ #[ command( subcommand) ]
133+ command : Commands ,
134+ }
135+
136+ // Parser for legacy flat structure (deprecated)
137+ #[ derive( Parser ) ]
138+ #[ clap( version, about, long_about = None ) ]
139+ struct CliFlatLegacy {
140+ #[ command( flatten) ]
141+ args : LadingArgs ,
142+ }
143+
144+ // Shared arguments used by both modes
145+ #[ derive( clap:: Args ) ]
130146#[ clap( group(
131147 ArgGroup :: new( "target" )
132148 . required( true )
@@ -142,7 +158,7 @@ impl FromStr for CliKeyValues {
142158 . required( false )
143159 . args( & [ "experiment_duration_seconds" , "experiment_duration_infinite" ] ) ,
144160) ) ]
145- struct Opts {
161+ struct LadingArgs {
146162 /// path on disk to the configuration file
147163 #[ clap( long, default_value_t = default_config_path( ) ) ]
148164 config_path : String ,
@@ -210,7 +226,19 @@ struct Opts {
210226 disable_inspector : bool ,
211227}
212228
213- fn get_config ( ops : & Opts , config : Option < String > ) -> Result < Config , Error > {
229+ #[ derive( Subcommand ) ]
230+ enum Commands {
231+ /// Run lading with specified configuration
232+ Run ( RunCommand ) ,
233+ }
234+
235+ #[ derive( Args ) ]
236+ struct RunCommand {
237+ #[ command( flatten) ]
238+ args : LadingArgs ,
239+ }
240+
241+ fn get_config ( args : & LadingArgs , config : Option < String > ) -> Result < Config , Error > {
214242 let contents = if let Some ( config) = config {
215243 config
216244 } else if let Ok ( env_var_value) = env:: var ( "LADING_CONFIG" ) {
@@ -219,13 +247,16 @@ fn get_config(ops: &Opts, config: Option<String>) -> Result<Config, Error> {
219247 } else {
220248 debug ! (
221249 "Attempting to open configuration file at: {}" ,
222- ops . config_path
250+ args . config_path
223251 ) ;
224252 let mut file: std:: fs:: File = std:: fs:: OpenOptions :: new ( )
225253 . read ( true )
226- . open ( & ops . config_path )
254+ . open ( & args . config_path )
227255 . unwrap_or_else ( |_| {
228- panic ! ( "Could not open configuration file at: {}" , & ops. config_path)
256+ panic ! (
257+ "Could not open configuration file at: {}" ,
258+ & args. config_path
259+ )
229260 } ) ;
230261 let mut contents = String :: new ( ) ;
231262 file. read_to_string ( & mut contents) ?;
@@ -235,52 +266,52 @@ fn get_config(ops: &Opts, config: Option<String>) -> Result<Config, Error> {
235266
236267 let mut config: Config = serde_yaml:: from_str ( & contents) ?;
237268
238- let target = if ops . no_target {
269+ let target = if args . no_target {
239270 None
240- } else if let Some ( pid) = ops . target_pid {
271+ } else if let Some ( pid) = args . target_pid {
241272 Some ( target:: Config :: Pid ( target:: PidConfig {
242273 pid : pid. try_into ( ) . expect ( "Could not convert pid to i32" ) ,
243274 } ) )
244- } else if let Some ( name) = & ops . target_container {
275+ } else if let Some ( name) = & args . target_container {
245276 Some ( target:: Config :: Docker ( target:: DockerConfig {
246277 name : name. clone ( ) ,
247278 } ) )
248- } else if let Some ( path) = & ops . target_path {
279+ } else if let Some ( path) = & args . target_path {
249280 Some ( target:: Config :: Binary ( target:: BinaryConfig {
250281 command : path. clone ( ) ,
251- arguments : ops . target_arguments . clone ( ) ,
252- inherit_environment : ops . target_inherit_environment ,
253- environment_variables : ops
282+ arguments : args . target_arguments . clone ( ) ,
283+ inherit_environment : args . target_inherit_environment ,
284+ environment_variables : args
254285 . target_environment_variables
255286 . clone ( )
256287 . unwrap_or_default ( )
257288 . inner ,
258289 output : Output {
259- stderr : ops . target_stderr_path . clone ( ) ,
260- stdout : ops . target_stdout_path . clone ( ) ,
290+ stderr : args . target_stderr_path . clone ( ) ,
291+ stdout : args . target_stdout_path . clone ( ) ,
261292 } ,
262293 } ) )
263294 } else {
264295 unreachable ! ( "clap ensures that exactly one target option is selected" ) ;
265296 } ;
266297 config. target = target;
267298
268- let options_global_labels = ops . global_labels . clone ( ) . unwrap_or_default ( ) ;
269- if let Some ( ref prom_addr) = ops . prometheus_addr {
299+ let options_global_labels = args . global_labels . clone ( ) . unwrap_or_default ( ) ;
300+ if let Some ( ref prom_addr) = args . prometheus_addr {
270301 config. telemetry = Telemetry :: Prometheus {
271302 addr : prom_addr. parse ( ) ?,
272303 global_labels : options_global_labels. inner ,
273304 } ;
274- } else if let Some ( ref prom_path) = ops . prometheus_path {
305+ } else if let Some ( ref prom_path) = args . prometheus_path {
275306 config. telemetry = Telemetry :: PrometheusSocket {
276307 path : prom_path. parse ( ) . map_err ( |_| Error :: PrometheusPath ) ?,
277308 global_labels : options_global_labels. inner ,
278309 } ;
279- } else if let Some ( ref capture_path) = ops . capture_path {
310+ } else if let Some ( ref capture_path) = args . capture_path {
280311 config. telemetry = Telemetry :: Log {
281312 path : capture_path. parse ( ) . map_err ( |_| Error :: CapturePath ) ?,
282313 global_labels : options_global_labels. inner ,
283- expiration : Duration :: from_secs ( ops . capture_expiriation_seconds . unwrap_or ( u64:: MAX ) ) ,
314+ expiration : Duration :: from_secs ( args . capture_expiriation_seconds . unwrap_or ( u64:: MAX ) ) ,
284315 } ;
285316 } else {
286317 match config. telemetry {
@@ -572,21 +603,34 @@ fn main() -> Result<(), Error> {
572603
573604 let version = env ! ( "CARGO_PKG_VERSION" ) ;
574605 info ! ( "Starting lading {version} run." ) ;
575- let opts: Opts = Opts :: parse ( ) ;
576606
577- let config = get_config ( & opts, None ) ;
607+ // Two-parser fallback logic until CliFlatLegacy is removed
608+ let args = match CliWithSubcommands :: try_parse ( ) {
609+ Ok ( cli) => match cli. command {
610+ Commands :: Run ( run_cmd) => run_cmd. args ,
611+ } ,
612+ Err ( _) => {
613+ // Fall back to legacy parsing
614+ match CliFlatLegacy :: try_parse ( ) {
615+ Ok ( legacy) => legacy. args ,
616+ Err ( err) => err. exit ( ) ,
617+ }
618+ }
619+ } ;
620+
621+ let config = get_config ( & args, None ) ;
578622
579- let experiment_duration = if opts . experiment_duration_infinite {
623+ let experiment_duration = if args . experiment_duration_infinite {
580624 Duration :: MAX
581625 } else {
582- Duration :: from_secs ( opts . experiment_duration_seconds . into ( ) )
626+ Duration :: from_secs ( args . experiment_duration_seconds . into ( ) )
583627 } ;
584628
585- let warmup_duration = Duration :: from_secs ( opts . warmup_duration_seconds . into ( ) ) ;
629+ let warmup_duration = Duration :: from_secs ( args . warmup_duration_seconds . into ( ) ) ;
586630 // The maximum shutdown delay is shared between `inner_main` and this
587631 // function, hence the divide by two.
588- let max_shutdown_delay = Duration :: from_secs ( opts . max_shutdown_delay . into ( ) ) ;
589- let disable_inspector = opts . disable_inspector ;
632+ let max_shutdown_delay = Duration :: from_secs ( args . max_shutdown_delay . into ( ) ) ;
633+ let disable_inspector = args . disable_inspector ;
590634
591635 let runtime = Builder :: new_multi_thread ( )
592636 . enable_io ( )
@@ -627,8 +671,8 @@ generator: []
627671 let capture_arg = format ! ( "--capture-path={}" , capture_path. display( ) ) ;
628672
629673 let args = vec ! [ "lading" , "--no-target" , capture_arg. as_str( ) ] ;
630- let ops : & Opts = & Opts :: parse_from ( args) ;
631- let config = get_config ( ops , Some ( contents. to_string ( ) ) ) ;
674+ let legacy_cli = CliFlatLegacy :: parse_from ( args) ;
675+ let config = get_config ( & legacy_cli . args , Some ( contents. to_string ( ) ) ) ;
632676 let exit_code = inner_main (
633677 Duration :: from_millis ( 2500 ) ,
634678 Duration :: from_millis ( 5000 ) ,
0 commit comments