14
14
15
15
use core:: fmt;
16
16
use std:: borrow:: Cow ;
17
- use std:: collections:: { BTreeMap , HashSet } ;
17
+ use std:: collections:: { BTreeMap , HashMap , HashSet } ;
18
18
use std:: env:: { self , ArgsOs , VarError } ;
19
19
use std:: ffi:: OsString ;
20
20
use std:: fmt:: Debug ;
@@ -2599,12 +2599,16 @@ fn resolve_default_command(
2599
2599
Ok ( string_args)
2600
2600
}
2601
2601
2602
- fn resolve_aliases (
2602
+ type AliasMap = HashMap < String , Vec < String > > ;
2603
+
2604
+ // NOTE: logging doesn't work in this function because we don't enable it until
2605
+ // after expanding arguments.
2606
+ fn expand_aliases (
2603
2607
ui : & Ui ,
2604
2608
config : & config:: Config ,
2605
2609
app : & Command ,
2606
2610
mut string_args : Vec < String > ,
2607
- ) -> Result < Vec < String > , CommandError > {
2611
+ ) -> Result < ( Vec < String > , AliasMap ) , CommandError > {
2608
2612
let mut aliases_map = config. get_table ( "aliases" ) ?;
2609
2613
if let Ok ( alias_map) = config. get_table ( "alias" ) {
2610
2614
for ( alias, definition) in alias_map {
@@ -2618,21 +2622,31 @@ fn resolve_aliases(
2618
2622
}
2619
2623
}
2620
2624
2621
- let mut resolved_aliases = HashSet :: new ( ) ;
2625
+ let mut expanded_aliases = HashSet :: new ( ) ;
2622
2626
let mut real_commands = HashSet :: new ( ) ;
2623
2627
for command in app. get_subcommands ( ) {
2624
2628
real_commands. insert ( command. get_name ( ) . to_string ( ) ) ;
2625
2629
for alias in command. get_all_aliases ( ) {
2626
2630
real_commands. insert ( alias. to_string ( ) ) ;
2627
2631
}
2628
2632
}
2629
- for alias in aliases_map. keys ( ) {
2630
- if real_commands. contains ( alias) {
2633
+
2634
+ let mut resolved_alias_map = HashMap :: new ( ) ;
2635
+ for ( alias_name, value) in aliases_map {
2636
+ if real_commands. contains ( & alias_name) {
2631
2637
writeln ! (
2632
2638
ui. warning_default( ) ,
2633
- "Cannot define an alias that overrides the built-in command '{alias }'"
2639
+ "Cannot define an alias that overrides the built-in command '{alias_name }'"
2634
2640
) ?;
2641
+ continue ;
2635
2642
}
2643
+
2644
+ let Ok ( alias_definition) = value. try_deserialize :: < Vec < String > > ( ) else {
2645
+ return Err ( user_error ( format ! (
2646
+ r#"Alias definition for "{alias_name}" must be a string list"#
2647
+ ) ) ) ;
2648
+ } ;
2649
+ resolved_alias_map. insert ( alias_name, alias_definition) ;
2636
2650
}
2637
2651
2638
2652
loop {
@@ -2646,32 +2660,106 @@ fn resolve_aliases(
2646
2660
. unwrap_or_default ( )
2647
2661
. map ( |arg| arg. to_str ( ) . unwrap ( ) . to_string ( ) )
2648
2662
. collect_vec ( ) ;
2649
- if resolved_aliases . contains ( & alias_name) {
2663
+ if expanded_aliases . contains ( & alias_name) {
2650
2664
return Err ( user_error ( format ! (
2651
2665
r#"Recursive alias definition involving "{alias_name}""#
2652
2666
) ) ) ;
2653
2667
}
2654
- if let Some ( value) = aliases_map. remove ( & alias_name) {
2655
- if let Ok ( alias_definition) = value. try_deserialize :: < Vec < String > > ( ) {
2656
- assert ! ( string_args. ends_with( & alias_args) ) ;
2657
- string_args. truncate ( string_args. len ( ) - 1 - alias_args. len ( ) ) ;
2658
- string_args. extend ( alias_definition) ;
2659
- string_args. extend_from_slice ( & alias_args) ;
2660
- resolved_aliases. insert ( alias_name. clone ( ) ) ;
2661
- continue ;
2662
- } else {
2663
- return Err ( user_error ( format ! (
2664
- r#"Alias definition for "{alias_name}" must be a string list"#
2665
- ) ) ) ;
2666
- }
2668
+ if let Some ( alias_definition) = resolved_alias_map. get ( & alias_name) {
2669
+ assert ! ( string_args. ends_with( & alias_args) ) ;
2670
+ string_args. truncate ( string_args. len ( ) - 1 - alias_args. len ( ) ) ;
2671
+ string_args. extend ( alias_definition. clone ( ) ) ;
2672
+ string_args. extend_from_slice ( & alias_args) ;
2673
+ expanded_aliases. insert ( alias_name. clone ( ) ) ;
2674
+ continue ;
2667
2675
} else {
2668
2676
// Not a real command and not an alias, so return what we've resolved so far
2669
- return Ok ( string_args ) ;
2677
+ break ;
2670
2678
}
2671
2679
}
2672
2680
}
2673
2681
// No more alias commands, or hit unknown option
2674
- return Ok ( string_args) ;
2682
+ break ;
2683
+ }
2684
+
2685
+ Ok ( ( string_args, resolved_alias_map) )
2686
+ }
2687
+
2688
+ fn add_alias_help ( app : & mut Command , mut aliases : AliasMap ) {
2689
+ let render_definition = |def : & [ String ] | match shlex:: try_join ( def. iter ( ) . map ( |s| & * * s) ) {
2690
+ Ok ( s) => format ! ( "Alias for \" {s}\" " ) ,
2691
+ Err ( _) => format ! ( "Alias for {def:?}" ) ,
2692
+ } ;
2693
+
2694
+ // Add each alias to `Command` so it shows up in help
2695
+ // Aliases may be defined in terms of other aliases, and it's allowed for them
2696
+ // to be defined in any order in the TOML config. Repeat this algorithm
2697
+ // until we no longer make progress.
2698
+ let mut progress;
2699
+ let mut try_again_later = HashMap :: new ( ) ;
2700
+ loop {
2701
+ progress = false ;
2702
+ for ( alias_name, alias_definition) in aliases. drain ( ) {
2703
+ // Find the innermost subcommand so we can use its help.
2704
+ let mut subcmd = & mut * app;
2705
+ let mut depth = 0 ;
2706
+ for arg in & alias_definition {
2707
+ if subcmd. find_subcommand_mut ( arg) . is_some ( ) {
2708
+ // hack: without -Zpolonius, the borrow checker thinks the command in the if
2709
+ // condition isn't assignable
2710
+ subcmd = subcmd. find_subcommand_mut ( arg) . unwrap ( ) ;
2711
+ depth += 1 ;
2712
+ } else {
2713
+ break ;
2714
+ }
2715
+ }
2716
+
2717
+ if depth == 0 {
2718
+ // We never found a valid subcommand. Skip this iteration; maybe it's an alias
2719
+ // to another alias.
2720
+ try_again_later. insert ( alias_name, alias_definition) ;
2721
+ continue ;
2722
+ } ;
2723
+ progress = true ;
2724
+
2725
+ if depth == 1 && alias_definition. len ( ) == 1 {
2726
+ // if this definition has no arguments, we can add it as a `visible_alias`,
2727
+ // which renders nicer. note that this only works for the root
2728
+ // command; for some reason, `visible_alias` does not seem to propagate from
2729
+ // subcommands to the root :(
2730
+ * subcmd = subcmd. clone ( ) . visible_alias ( alias_name) ;
2731
+ } else {
2732
+ // have to add a standalone subcmd
2733
+ let help = render_definition ( & alias_definition) ;
2734
+ let subcmd_alias = subcmd
2735
+ . clone ( )
2736
+ . name ( alias_name)
2737
+ // if we leave the built-in aliases, clap doesn't know whether to call the user
2738
+ // alias or the built-in command
2739
+ . alias ( None )
2740
+ // TODO: for recursive aliases, it would be nice to show the full args this
2741
+ // expands to rather than just the immediately next alias
2742
+ . before_help ( & help)
2743
+ . before_long_help ( help) ;
2744
+ * app = std:: mem:: take ( app) . subcommand ( subcmd_alias) ;
2745
+ }
2746
+ }
2747
+ if !progress {
2748
+ break ;
2749
+ }
2750
+ aliases. extend ( try_again_later. drain ( ) ) ;
2751
+ }
2752
+ // We may still have some unhandled aliases. That means there's an alias with
2753
+ // valid toml syntax, but points to a subcommand that doesn't exist. Add
2754
+ // help for that too.
2755
+ for ( name, definition) in try_again_later {
2756
+ let help = render_definition ( & definition) ;
2757
+ // TODO: we can't distinguish empty/option-only aliases from aliases without a
2758
+ // subcommand :( This leaves the default "global options" help in case
2759
+ // it does happen to be valid. Perhaps we should do
2760
+ // `app.try_parse_matches` here so we can distinguish the two?
2761
+ let subcmd = Command :: new ( name) . about ( help) ;
2762
+ * app = std:: mem:: take ( app) . subcommand ( subcmd) ;
2675
2763
}
2676
2764
}
2677
2765
@@ -2713,7 +2801,7 @@ pub fn expand_args(
2713
2801
app : & Command ,
2714
2802
args_os : ArgsOs ,
2715
2803
config : & config:: Config ,
2716
- ) -> Result < Vec < String > , CommandError > {
2804
+ ) -> Result < ( Vec < String > , AliasMap ) , CommandError > {
2717
2805
let mut string_args: Vec < String > = vec ! [ ] ;
2718
2806
for arg_os in args_os {
2719
2807
if let Some ( string_arg) = arg_os. to_str ( ) {
@@ -2724,7 +2812,7 @@ pub fn expand_args(
2724
2812
}
2725
2813
2726
2814
let string_args = resolve_default_command ( ui, config, app, string_args) ?;
2727
- resolve_aliases ( ui, config, app, string_args)
2815
+ expand_aliases ( ui, config, app, string_args)
2728
2816
}
2729
2817
2730
2818
pub fn parse_args (
@@ -2903,7 +2991,7 @@ impl CliRunner {
2903
2991
2904
2992
#[ instrument( skip_all) ]
2905
2993
fn run_internal (
2906
- self ,
2994
+ mut self ,
2907
2995
ui : & mut Ui ,
2908
2996
mut layered_configs : LayeredConfigs ,
2909
2997
) -> Result < ( ) , CommandError > {
@@ -2929,7 +3017,11 @@ impl CliRunner {
2929
3017
let config = layered_configs. merge ( ) ;
2930
3018
ui. reset ( & config) ?;
2931
3019
2932
- let string_args = expand_args ( ui, & self . app , env:: args_os ( ) , & config) ?;
3020
+ // save this - since `expand_args` modifies app, it won't parse the same when we
3021
+ // process -R
3022
+ let original_app = self . app . clone ( ) ;
3023
+ let ( string_args, aliases) = expand_args ( ui, & self . app , env:: args_os ( ) , & config) ?;
3024
+ add_alias_help ( & mut self . app , aliases) ;
2933
3025
let ( matches, args) = parse_args (
2934
3026
ui,
2935
3027
& self . app ,
@@ -2959,7 +3051,9 @@ impl CliRunner {
2959
3051
// If -R is specified, check if the expanded arguments differ. Aliases
2960
3052
// can also be injected by --config-toml, but that's obviously wrong.
2961
3053
if args. global_args . repository . is_some ( ) {
2962
- let new_string_args = expand_args ( ui, & self . app , env:: args_os ( ) , & config) . ok ( ) ;
3054
+ let new_string_args = expand_args ( ui, & original_app, env:: args_os ( ) , & config)
3055
+ . ok ( )
3056
+ . map ( |( args, _) | args) ;
2963
3057
if new_string_args. as_ref ( ) != Some ( & string_args) {
2964
3058
writeln ! (
2965
3059
ui. warning_default( ) ,
0 commit comments