Skip to content

Commit d75723d

Browse files
authored
Merge pull request #515 from tbidne/commands
Implement command aliases. Thanks!
2 parents db1bf65 + 7b66b8c commit d75723d

17 files changed

Lines changed: 407 additions & 10 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- Remove `fullDesc` and `briefDesc` builder modifiers – they have not had an
77
effect since version 0.8.
88

9+
- Add `commandWithAliases` for defining multiple aliases for the same command.
10+
911
## Version 0.19.0.0 (03 June 2025)
1012

1113
- Add `briefHangPoint` modifier. This allows one to specify the command length

optparse-applicative.cabal

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ extra-source-files: CHANGELOG.md
4545
tests/parser_group_duplicate_command_groups.err.txt
4646
tests/parser_group_duplicates.err.txt
4747
tests/parser_group_nested.err.txt
48+
tests/prop_cmd_alias_dupes.err.txt
49+
tests/prop_cmd_aliases.err.txt
50+
tests/prop_cmd_dupes.err.txt
4851
tests/nested_optional.err.txt
4952
tests/subparsers.err.txt
5053

@@ -135,7 +138,10 @@ test-suite tests
135138

136139
other-modules: Examples.Alternatives
137140
, Examples.Cabal
141+
, Examples.CommandAliasDupes
142+
, Examples.CommandDupes
138143
, Examples.Commands
144+
, Examples.CommandAliases
139145
, Examples.Formatting
140146
, Examples.Hello
141147
, Examples.LongSub

src/Options/Applicative.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ module Options.Applicative (
9999
internal,
100100
style,
101101
command,
102+
commandWithAliases,
102103
commandGroup,
103104
completeWith,
104105
action,

src/Options/Applicative/BashCompletion.hs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ import Prelude
1717
#if !defined(__MHS__)
1818
import Data.Foldable ( asum )
1919
#endif
20+
21+
import qualified Data.Foldable as Foldable
2022
import Data.List ( isPrefixOf )
21-
import Data.Maybe ( fromMaybe, listToMaybe )
23+
import Data.List.NonEmpty (NonEmpty)
24+
import Data.Maybe ( fromMaybe, listToMaybe, mapMaybe )
2225

2326
import Options.Applicative.Builder
2427
import Options.Applicative.Common
@@ -120,7 +123,7 @@ bashCompletionQuery pinfo pprefs richness ws i _ = case runCompletion compl ppre
120123
| argumentIsUnreachable reachability
121124
-> return []
122125
| otherwise
123-
-> return . with_cmd_help $ filter (is_completion . fst) ns
126+
-> return . with_cmd_help $ filter_completions ns
124127

125128
-- When doing enriched completions, add any help specified
126129
-- to the completion variables (tab separated).
@@ -148,6 +151,30 @@ bashCompletionQuery pinfo pprefs richness ws i _ = case runCompletion compl ppre
148151
show_names :: [OptName] -> [String]
149152
show_names = filter is_completion . map showOption
150153

154+
-- Filter commands and aliases matching the completion e.g. if we have
155+
-- commands:
156+
--
157+
-- - ([retry], p1)
158+
-- - ([run, r, go], p2)
159+
-- - ([search], p3)
160+
--
161+
-- and the user types 'r', we should return
162+
--
163+
-- [(retry, p1), (run, p2)]
164+
--
165+
-- If the first entry (command name) is the canonical one, we should
166+
-- favour it in completions, as if we were to provide all options there
167+
-- would be a good chance we'd be forcing the user to tab through and
168+
-- disambiguate between functionally identical options.
169+
--
170+
-- In zsh and fish we also provide the help doc in the completions, which
171+
-- we don't want to repeat a whole bunch of times.
172+
filter_completions :: [(NonEmpty String, ParserInfo a)] -> [(String, ParserInfo a)]
173+
filter_completions =
174+
let findAlias :: (NonEmpty String, b) -> Maybe (String, b)
175+
findAlias (aliases, painfo) = (\x -> (x, painfo)) <$> Foldable.find is_completion aliases
176+
in mapMaybe findAlias
177+
151178
-- We only want to show a single line in the completion results description.
152179
-- If there was a line break, it would come across as a different completion
153180
-- possibility.

src/Options/Applicative/Builder.hs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module Options.Applicative.Builder (
4343
internal,
4444
style,
4545
command,
46+
commandWithAliases,
4647
commandGroup,
4748
completeWith,
4849
action,
@@ -107,6 +108,7 @@ module Options.Applicative.Builder (
107108
) where
108109

109110
import Control.Applicative
111+
import Data.List.NonEmpty (NonEmpty ((:|)))
110112
#if __GLASGOW_HASKELL__ < 804
111113
import Data.Semigroup hiding (Option, option)
112114
#endif
@@ -240,7 +242,28 @@ style x = optionMod $ \p ->
240242
-- @
241243
command :: String -> ParserInfo a -> Mod CommandFields a
242244
command cmd pinfo = fieldMod $ \p ->
243-
p { cmdCommands = (cmd, pinfo) : cmdCommands p }
245+
p { cmdCommands = (cmd :| [], pinfo) : cmdCommands p }
246+
247+
-- | Add a command and possible aliases to a subparser option.
248+
--
249+
-- @
250+
-- sample :: Parser Sample
251+
-- sample = subparser
252+
-- ( commandWithAliases ("hello" :| ["hi"])
253+
-- (info hello (progDesc "Print greeting"))
254+
-- <> command "goodbye"
255+
-- (info goodbye (progDesc "Say goodbye"))
256+
-- )
257+
-- @
258+
--
259+
-- > Available commands:
260+
-- > hello, hi Print greeting
261+
-- > goodbye Say goodbye
262+
--
263+
-- @since 0.20.0.0
264+
commandWithAliases :: NonEmpty String -> ParserInfo a -> Mod CommandFields a
265+
commandWithAliases aliases pinfo = fieldMod $ \p ->
266+
p { cmdCommands = (aliases, pinfo) : cmdCommands p }
244267

245268
-- | Add a description to a group of commands.
246269
--

src/Options/Applicative/Builder/Internal.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module Options.Applicative.Builder.Internal (
2626

2727
import Control.Applicative
2828
import Control.Monad (mplus)
29+
import Data.List.NonEmpty (NonEmpty)
2930
import Data.Semigroup hiding (Option)
3031
import Prelude
3132

@@ -42,7 +43,7 @@ data FlagFields a = FlagFields
4243
, flagActive :: a }
4344

4445
data CommandFields a = CommandFields
45-
{ cmdCommands :: [(String, ParserInfo a)]
46+
{ cmdCommands :: [(NonEmpty String, ParserInfo a)]
4647
, cmdGroup :: Maybe String }
4748

4849
data ArgumentFields a = ArgumentFields
@@ -154,7 +155,7 @@ baseProps = OptProperties
154155
, propGroup = OptGroup []
155156
}
156157

157-
mkCommand :: Mod CommandFields a -> (Maybe String, [(String, ParserInfo a)])
158+
mkCommand :: Mod CommandFields a -> (Maybe String, [(NonEmpty String, ParserInfo a)])
158159
mkCommand m = (group, cmds)
159160
where
160161
Mod f _ _ = m

src/Options/Applicative/Common.hs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE Rank2Types #-}
2+
23
module Options.Applicative.Common (
34
-- * Option parsers
45
--
@@ -55,7 +56,9 @@ import Control.Applicative
5556
import Control.Monad (guard, mzero, msum, when)
5657
import Control.Monad.Trans.Class (lift)
5758
import Control.Monad.Trans.State (StateT(..), get, put, runStateT)
59+
import qualified Data.Foldable as F
5860
import Data.List (isPrefixOf)
61+
import Data.List.NonEmpty (NonEmpty)
5962
import Data.Maybe (maybeToList, isJust, isNothing)
6063
import Prelude
6164

@@ -195,8 +198,16 @@ searchArg prefs arg =
195198

196199
where
197200
cmdMatches cs
198-
| prefDisambiguate prefs = snd <$> filter (isPrefixOf arg . fst) cs
199-
| otherwise = maybeToList (lookup arg cs)
201+
| prefDisambiguate prefs = snd <$> filter (F.any (isPrefixOf arg) . fst) cs
202+
| otherwise = maybeToList (lookupCmd arg cs)
203+
204+
lookupCmd :: String -> [(NonEmpty String, a)] -> Maybe a
205+
lookupCmd k = foldr go Nothing
206+
where
207+
go (aliases, y) acc =
208+
if F.any (== k) aliases
209+
then Just y
210+
else acc
200211

201212
stepParser :: MonadP m => ParserPrefs -> ArgPolicy -> String
202213
-> Parser a -> NondetT (StateT Args m) (Parser a)

src/Options/Applicative/Extra.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Control.Applicative
2525
import Control.Monad (void)
2626
import Data.Monoid
2727
import Data.Foldable (traverse_)
28+
import qualified Data.List.NonEmpty as NE
2829
import Prelude
2930
import System.Environment (getArgs, getProgName)
3031
import System.Exit (exitSuccess, exitWith, ExitCode(..))
@@ -320,7 +321,7 @@ parserFailure pprefs pinfo msg ctx0 = ParserFailure $ \progn ->
320321
CmdReader _ ns | argumentIsUnreachable reachability
321322
-> []
322323
| otherwise
323-
-> fst <$> ns
324+
-> ns >>= NE.toList . fst
324325
_
325326
-> mempty
326327

src/Options/Applicative/Help/Core.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,16 @@ cmdDesc pprefs = mapParser desc
9696
CmdReader gn cmds ->
9797
(,) gn $
9898
tabulate (prefTabulateFill pprefs)
99-
[ (pretty nm, align (extractChunk (infoProgDesc cmd)))
99+
[ (pCmds nm, align (extractChunk (infoProgDesc cmd)))
100100
| (nm, cmd) <- reverse cmds
101101
]
102102
_ -> mempty
103103

104+
pCmds =
105+
pretty
106+
. List.intercalate ","
107+
. NE.toList
108+
104109
-- | Generate a brief help text for a parser.
105110
briefDesc :: ParserPrefs -> Parser a -> Chunk Doc
106111
briefDesc = briefDesc' True

src/Options/Applicative/Types.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import Control.Monad.Trans.Except (Except, throwE)
5656
import Control.Monad.Trans.Class (lift)
5757
import Control.Monad.Trans.Reader (ReaderT, ask)
5858
import qualified Control.Monad.Fail as Fail
59+
import Data.List.NonEmpty (NonEmpty)
5960
import Data.Semigroup hiding (Option)
6061
import Prelude
6162

@@ -273,7 +274,7 @@ data OptReader a
273274
-- ^ flag reader
274275
| ArgReader (CReader a)
275276
-- ^ argument reader
276-
| CmdReader (Maybe String) [(String, ParserInfo a)]
277+
| CmdReader (Maybe String) [(NonEmpty String, ParserInfo a)]
277278
-- ^ command reader
278279

279280
instance Functor OptReader where

0 commit comments

Comments
 (0)