Skip to content

Commit 02d0b60

Browse files
committed
ADR-012 Standardise CLI multiple choice flags construction
1 parent 9115ead commit 02d0b60

File tree

1 file changed

+225
-0
lines changed

1 file changed

+225
-0
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Status
2+
3+
📜 Proposed 2025-04-14
4+
5+
# Context
6+
7+
The `cardano-cli` has grown to support many commands, each exposing a variety of options and flags. Over time, the way flags are specified has become inconsistent.
8+
9+
Examples of inconsistency include:
10+
11+
## Multiple conventions for how flags are specified
12+
13+
Sometimes we make see the switch with argument form `--output-format text` and sometimes
14+
the simple switch form `--output-text`.
15+
16+
## Multiple default values for a choice
17+
18+
Having more than one default is confusing and complicated. For example it is possible to have
19+
both `--output-text` and `--output-json` to be the defaults for the same command depending on
20+
how the command is use.
21+
22+
## Actual default behaviour is determined by another flag.
23+
24+
When there is more than default, another flag decides which selection out of possible choices
25+
is the default. For example `--output-file` when unspecified would make the default `--output-text`
26+
but when specified would make the default `--output-json`
27+
28+
## Default behaviour determination is determined by the run command
29+
30+
When the command data structure is fully constructed and passed to the run command, it is not
31+
fully known what choice is to be made if it was unspecified. Instead it is left to the run
32+
command to decide. This can lead to inconsistency between CLI documentation and what is
33+
implemented in the run command.
34+
35+
## We have multiple types for choices
36+
37+
That are similar and differ only in name or are plus/minus some constructors:
38+
39+
```haskell
40+
data OutputFormatJsonOrText
41+
= OutputFormatJson
42+
| OutputFormatText
43+
deriving (Eq, Show)
44+
45+
data AllOutputFormats
46+
= FormatJson
47+
| FormatText
48+
| FormatCBOR
49+
deriving Show
50+
51+
data ViewOutputFormat
52+
= ViewOutputFormatJson
53+
| ViewOutputFormatYaml
54+
deriving Show
55+
56+
data FriendlyFormat = FriendlyJson | FriendlyYaml
57+
```
58+
59+
## Inconsistent ordering of choices
60+
61+
As we use multiple parsers for similar kinds of things it is possible for the ordering to
62+
be inconsistent.
63+
64+
## Example
65+
66+
```haskell
67+
data QueryUTxOCmdArgs = QueryUTxOCmdArgs
68+
{ ...
69+
, format :: Maybe AllOutputFormats --
70+
, mOutFile :: !(Maybe (File () Out))
71+
}
72+
deriving (Generic, Show)
73+
74+
pQueryUTxOCmd :: ShelleyBasedEra era -> EnvCli -> Parser (QueryCmds era)
75+
pQueryUTxOCmd era envCli =
76+
fmap QueryUTxOCmd $
77+
QueryUTxOCmdArgs
78+
<$> pQueryCommons era envCli
79+
<*> pQueryUTxOFilter
80+
<*> ( optional $ -- absence of explicit choice means default by which one?
81+
asum -- choice of output format includes two defaults, inconsistent ordering
82+
[ pFormatCBOR "utxo"
83+
, pFormatTextDefault "utxo" -- default 1
84+
, pFormatJsonDefault "utxo" -- default 2
85+
]
86+
)
87+
<*> pMaybeOutputFile -- The default is determined whether the output-file is specified,
88+
-- but this is non-obvious and we still don't know the default.
89+
90+
runQueryUTxOCmd
91+
:: ()
92+
=> Cmd.QueryUTxOCmdArgs
93+
-> ExceptT QueryCmdError IO ()
94+
runQueryUTxOCmd
95+
( Cmd.QueryUTxOCmdArgs
96+
{ ...
97+
, Cmd.format
98+
, Cmd.mOutFile
99+
}
100+
) = do
101+
join $
102+
lift
103+
( executeLocalStateQueryExpr nodeConnInfo target $ runExceptT $ do
104+
...
105+
106+
pure $ do
107+
writeFilteredUTxOs sbe format mOutFile utxo -- code to decide the default is embedded in here
108+
-- far away from the CLI specification which makes
109+
-- bugs non-obvious
110+
)
111+
& onLeft (left . QueryCmdAcquireFailure)
112+
& onLeft left
113+
```
114+
115+
# Decision
116+
117+
We will adopt a standardized approach for CLI flag specification:
118+
119+
## Where possible, always use the simple switch form
120+
121+
This means the flag should be of the form `--output-text` instead of `--output-format text`.
122+
123+
## Do not allow more than one default
124+
125+
There should be no more than one default and it should be fixed and visible from the help.
126+
127+
The ability to do this in a clean and less error prone way is shown by the following points.
128+
129+
## Where the choice options are not fixed, use Vary
130+
131+
We may have for example the following options:
132+
133+
* `FormatCbor`
134+
* `FormatJson`
135+
* `FormatText`
136+
* `FormatYaml`
137+
138+
But different commands may only allow some subset of them and different commands use different
139+
subsets.
140+
141+
In this case, do not define a sum type like this:
142+
143+
```haskell
144+
data Format = FormatCbor | FormatJson | FormatText | FormatYaml
145+
```
146+
147+
Insteade use the `Vary` type to choose the options you want to include for any given command.
148+
149+
For example:
150+
151+
```haskell
152+
data QueryUTxOCmdArgs = QueryUTxOCmdArgs
153+
{ ...
154+
, format :: Vary [FormatCBOR, FormatJson, FormatText]
155+
}
156+
```
157+
158+
## When using Vary, keep the options in alphabetical order
159+
160+
This will ensure that the help text presented to the user has consistent ordering.
161+
162+
## Define flags as values of `Flag a`, not `Parser a`
163+
164+
Using `Flag a` provides a cleaner way to specify the default.
165+
166+
```haskell
167+
flagFormatCbor :: FormatCbor :| fs => Flag (Vary fs)
168+
flagFormatCbor = mkFlag "output-cbor" "CBOR" FormatCbor
169+
170+
flagFormatJson :: FormatCBOR :| fs => Flag (Vary fs)
171+
flagFormatJson = mkFlag "output-cbor" "BASE16 CBOR" FormatCBOR
172+
173+
flagFormatText :: FormatText :| fs => Flag (Vary fs)
174+
flagFormatText = mkFlag "output-text" "TEXT" FormatText
175+
```
176+
177+
## Construct CLI parsers for choices using parserFromFormatFlags
178+
179+
```haskell
180+
pQueryUTxOCmd :: ShelleyBasedEra era -> EnvCli -> Parser (QueryCmds era)
181+
pQueryUTxOCmd era envCli =
182+
fmap QueryUTxOCmd $
183+
QueryUTxOCmdArgs
184+
<$> ...
185+
<*> parserFromFormatFlags
186+
"utxo query output"
187+
[ flagFormatCbor
188+
, flagFormatJson & isDefault
189+
, flagFormatText
190+
]
191+
```
192+
193+
## When there is a default, use `isDefault` combinator to specify it
194+
195+
The `isDefault` combinator modifies a flag to be the default.
196+
197+
```haskell
198+
pQueryUTxOCmd :: ShelleyBasedEra era -> EnvCli -> Parser (QueryCmds era)
199+
pQueryUTxOCmd era envCli =
200+
fmap QueryUTxOCmd $
201+
QueryUTxOCmdArgs
202+
<$> ...
203+
<*> parserFromFormatFlags
204+
"utxo query output"
205+
[ flagFormatCbor
206+
, flagFormatJson & isDefault -- this is the default
207+
, flagFormatText
208+
]
209+
```
210+
211+
# Consequences
212+
213+
### ✅ Positive
214+
215+
* *Declarative defaults*: Defaults are visible in the CLI parser, not buried in runtime logic.
216+
* *Consistent user experience*: CLI behavior and help messages are aligned across commands.
217+
* *Reduced duplication*: The `Vary` mechanism allows composable subsets of choices.
218+
* *Easier testing and documentation*: Defaults are discoverable without running the command.
219+
* *Fewer bugs*: Centralized handling makes incorrect or conflicting defaults less likely.
220+
221+
### ⚠️ Negative
222+
223+
* *Migration effort*: Existing commands must be updated to the new `Vary` + `Flag` approach.
224+
* *Learning curve*: Contributors will need to understand how `Flag`, `Vary`, and `isDefault` work.
225+
* *Breaking changes*: Some commands may behave differently after adopting this change.

0 commit comments

Comments
 (0)