Skip to content

Commit 09a7f8d

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

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+
Instead define separate types for each:
148+
149+
```haskell
150+
data FormatCbor = FormatCbor deriving (Eq, Show)
151+
data FormatJson = FormatJson deriving (Eq, Show)
152+
data FormatText = FormatText deriving (Eq, Show)
153+
```
154+
155+
Then use the `Vary` type to choose the options you want to include for any given command.
156+
157+
For example:
158+
159+
```haskell
160+
data QueryUTxOCmdArgs = QueryUTxOCmdArgs
161+
{ ...
162+
, format :: Vary [FormatCbor, FormatJson, FormatText]
163+
}
164+
```
165+
166+
## When using Vary, keep the options in alphabetical order
167+
168+
This will ensure that the help text presented to the user has consistent ordering.
169+
170+
## Define flags as values of `Flag a`, not `Parser a`
171+
172+
Using `Flag a` provides a cleaner way to specify the default.
173+
174+
```haskell
175+
flagFormatCbor :: FormatCbor :| fs => Flag (Vary fs)
176+
flagFormatCbor = mkFlag "output-cbor" "BASE16 CBOR" FormatCbor
177+
178+
flagFormatJson :: FormatJson :| fs => Flag (Vary fs)
179+
flagFormatJson = mkFlag "output-json" "JSON" FormatJson
180+
181+
flagFormatText :: FormatText :| fs => Flag (Vary fs)
182+
flagFormatText = mkFlag "output-text" "TEXT" FormatText
183+
```
184+
185+
## Construct CLI parsers for choices using parserFromFormatFlags
186+
187+
```haskell
188+
pQueryUTxOCmd :: ShelleyBasedEra era -> EnvCli -> Parser (QueryCmds era)
189+
pQueryUTxOCmd era envCli =
190+
fmap QueryUTxOCmd $
191+
QueryUTxOCmdArgs
192+
<$> ...
193+
<*> parserFromFormatFlags
194+
"utxo query output"
195+
[ flagFormatCbor
196+
, flagFormatJson
197+
, flagFormatText
198+
]
199+
```
200+
201+
## When there is a default, use `setDefault` combinator to specify it
202+
203+
The `setDefault` combinator modifies a flag to be the default.
204+
205+
```haskell
206+
pQueryUTxOCmd :: ShelleyBasedEra era -> EnvCli -> Parser (QueryCmds era)
207+
pQueryUTxOCmd era envCli =
208+
fmap QueryUTxOCmd $
209+
QueryUTxOCmdArgs
210+
<$> ...
211+
<*> parserFromFormatFlags
212+
"utxo query output"
213+
[ flagFormatCbor
214+
, flagFormatJson & setDefault -- this is the default
215+
, flagFormatText
216+
]
217+
```
218+
219+
# Consequences
220+
221+
### ✅ Positive
222+
223+
* *Declarative defaults*: Defaults are visible in the CLI parser, not buried in runtime logic.
224+
* *Consistent user experience*: CLI behavior and help messages are aligned across commands.
225+
* *Reduced duplication*: The `Vary` mechanism allows composable subsets of choices.
226+
* *Easier testing and documentation*: Defaults are discoverable without running the command.
227+
* *Fewer bugs*: Centralized handling makes incorrect or conflicting defaults less likely.
228+
229+
### ⚠️ Negative
230+
231+
* *Migration effort*: Existing commands must be updated to the new `Vary` + `Flag` approach.
232+
* *Learning curve*: Contributors will need to understand how `Flag`, `Vary`, and `setDefault` work.
233+
* *Breaking changes*: Some commands may behave differently after adopting this change.

0 commit comments

Comments
 (0)