Skip to content

Implement command aliases#515

Merged
HuwCampbell merged 2 commits into
pcapriotti:masterfrom
tbidne:commands
Apr 18, 2026
Merged

Implement command aliases#515
HuwCampbell merged 2 commits into
pcapriotti:masterfrom
tbidne:commands

Conversation

@tbidne

@tbidne tbidne commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

This implements the "command aliases" portion of #484 (the other part, default commands, is left for another day). My motivation is presumably similar to the OP of that issue i.e. I sometimes want shorter aliases for commands, much like we can do with options. For example:

-- options
long "file"
  <> short 'f'

-- new, for commands
-- commandAliases :: NonEmpty String -> ParserInfo a -> Mod CommandFields a
commandAliases
  ("print" :| ["p"])
  (info (pure Print) (progDesc "Runs print"))

I chose to implement this as a companion to the normal function:

command :: String -> ParserInfo a -> Mod CommandFields a

since CommandFields is a modifier for the command group, not an individual command, so we cannot monoidally combine names for an individual command, like we can for options.

This is technically a breaking change as Options.Applicative.Builder.Internal exports the CommandFields constructor:

diff --git a/src/Options/Applicative/Builder/Internal.hs b/src/Options/Applicative/Builder/Internal.hs
index 2110067..68984c2 100644
--- a/src/Options/Applicative/Builder/Internal.hs
+++ b/src/Options/Applicative/Builder/Internal.hs
 data CommandFields a = CommandFields
-  { cmdCommands :: [(String, ParserInfo a)]
+  { cmdCommands :: [(NonEmpty String, ParserInfo a)]
   , cmdGroup :: Maybe String }

Incidentally, it would be great to have #367, as imo this should really only require a minor bump, not a major one. I would be happy to investigate that, though there is less need for it wrt this PR, since we are already on a new major bump.

Thanks!

@tbidne tbidne force-pushed the commands branch 2 times, most recently from 0d212a3 to 9a17094 Compare April 15, 2026 00:07
@tbidne

tbidne commented Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

Some bikeshedding notes:

  1. Currently, this differs from the proposed rendering in the issue i.e. we have no whitespace between aliases:

    Usage: prop_cmd_aliases (COMMAND | COMMAND)
    
    Available options:
      -h,--help                Show this help text
    
    Available commands:
      hello,hi                 Print greeting
      goodbye                  Say goodbye
    
    French commands:
      bonjour                  Print greeting
      au-revoir,adieu,ciao     Say goodbye
    
    Other commands:
      health                   Check health
      aux                      Auxiliary
    

    This is for consistency with options (e.g. see -h,--help above), though it is arguably busy compared to the variant with a single space:

    Usage: prop_cmd_aliases (COMMAND | COMMAND)
    
    Available options:
      -h,--help                Show this help text
    
    Available commands:
      hello, hi                Print greeting
      goodbye                  Say goodbye
    
    French commands:
      bonjour                  Print greeting
      au-revoir, adieu, ciao   Say goodbye
    
    Other commands:
      health                   Check health
      aux                      Auxiliary
    
  2. There is no normalizing wrt repeated aliases, for instance, duplicate aliases will be listed in completions multiple times. This is consistent with how commands currently work e.g.

    command "cmd" (info (pure Cmd1) ...)
      <> command "cmd" (info (pure Cmd2) ...)

    Both will be listed and CLI cmd will choose Cmd2.

@tbidne

tbidne commented Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

The microhs error is baffling me:

mhs: uncaught exception: error: "src/Options/Applicative/Common.hs": line 200, col 48: Cannot satisfy constraint: ([] ~ NonEmpty)
     fully qualified: (Primitives.~ Data.List_Type.[] Data.List.NonEmpty_Type.NonEmpty)

It is complaining here:

    cmdMatches cs
      | prefDisambiguate prefs = snd <$> filter (any (isPrefixOf arg) . fst) cs
      --                                 HERE: vvv
      | otherwise = maybeToList (lookupCmd arg cs)

The types are:

cs :: [(NonEmpty String, ParserInfo r)]
lookupCmd :: String -> [(NonEmpty String, a)] -> Maybe a

🤷

@HuwCampbell

Copy link
Copy Markdown
Collaborator

Pretty sure that error is because micro haskell has a different type for any.

any :: forall a . (a -> Bool) -> [a] -> Bool

As opposed to it being over Foldable.

any :: Foldable t -> (a -> Bool) -> t a -> Bool

@HuwCampbell HuwCampbell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent as usual. Thanks.

I'll do some tyre kicking, but I see no current issues.

Comment thread src/Options/Applicative/Extra.hs Outdated
Comment thread src/Options/Applicative/Builder.hs
@tbidne

tbidne commented Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

Pretty sure that error is because micro haskell has a different type for any.

Bang on, fixed now.

Thanks for the review and kind words!

Comment thread src/Options/Applicative/BashCompletion.hs Outdated
@HuwCampbell HuwCampbell merged commit d75723d into pcapriotti:master Apr 18, 2026
24 of 27 checks passed
@tbidne

tbidne commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the merge!

@tbidne tbidne deleted the commands branch April 23, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants