Skip to content

Commit fc328af

Browse files
authored
Merge pull request #72 from input-output-hk/newhoggy/ADR-012
ADR-012 Standardise CLI multiple choice flags construction
2 parents 9115ead + e955576 commit fc328af

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
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 used.
21+
22+
## Actual default behaviour is determined by another flag.
23+
24+
When there is more than one 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`. Temporary support for backwards
122+
compatibility should be considered when migrating to the new style.
123+
124+
## Do not allow more than one default
125+
126+
There should be no more than one default and it should be fixed and visible from the help.
127+
128+
The ability to do this in a clean and less error prone way is shown by the following points.
129+
130+
## Where the choice options are not fixed, use Vary
131+
132+
We may have for example the following options:
133+
134+
* `FormatCbor`
135+
* `FormatJson`
136+
* `FormatText`
137+
* `FormatYaml`
138+
139+
But different commands may only allow some subset of them and different commands use different
140+
subsets.
141+
142+
In this case, do not define a sum type like this:
143+
144+
```haskell
145+
data Format = FormatCbor | FormatJson | FormatText | FormatYaml
146+
```
147+
148+
Instead define separate types for each:
149+
150+
```haskell
151+
data FormatCbor = FormatCbor deriving (Eq, Show)
152+
data FormatJson = FormatJson deriving (Eq, Show)
153+
data FormatText = FormatText deriving (Eq, Show)
154+
```
155+
156+
Then use the `Vary` type to choose the options you want to include for any given command.
157+
158+
For example:
159+
160+
```haskell
161+
data QueryUTxOCmdArgs = QueryUTxOCmdArgs
162+
{ ...
163+
, format :: Vary [FormatCbor, FormatJson, FormatText]
164+
}
165+
```
166+
167+
## When using Vary, keep the options in alphabetical order
168+
169+
This will ensure that the help text presented to the user has consistent ordering.
170+
171+
## Define flags as values of `Flag a`, not `Parser a`
172+
173+
Using `Flag a` provides a cleaner way to specify the default.
174+
175+
```haskell
176+
flagFormatCbor :: FormatCbor :| fs => Flag (Vary fs)
177+
flagFormatCbor = mkFlag "output-cbor" "BASE16 CBOR" FormatCbor
178+
179+
flagFormatJson :: FormatJson :| fs => Flag (Vary fs)
180+
flagFormatJson = mkFlag "output-json" "JSON" FormatJson
181+
182+
flagFormatText :: FormatText :| fs => Flag (Vary fs)
183+
flagFormatText = mkFlag "output-text" "TEXT" FormatText
184+
```
185+
186+
## Construct CLI parsers for choices using parserFromFormatFlags
187+
188+
```haskell
189+
pQueryUTxOCmd :: ShelleyBasedEra era -> EnvCli -> Parser (QueryCmds era)
190+
pQueryUTxOCmd era envCli =
191+
fmap QueryUTxOCmd $
192+
QueryUTxOCmdArgs
193+
<$> ...
194+
<*> parserFromFormatFlags
195+
"utxo query output"
196+
[ flagFormatCbor
197+
, flagFormatJson
198+
, flagFormatText
199+
]
200+
```
201+
202+
## When there is a default, use `setDefault` combinator to specify it
203+
204+
The `setDefault` combinator modifies a flag to be the default.
205+
206+
```haskell
207+
pQueryUTxOCmd :: ShelleyBasedEra era -> EnvCli -> Parser (QueryCmds era)
208+
pQueryUTxOCmd era envCli =
209+
fmap QueryUTxOCmd $
210+
QueryUTxOCmdArgs
211+
<$> ...
212+
<*> parserFromFormatFlags
213+
"utxo query output"
214+
[ flagFormatCbor
215+
, flagFormatJson & setDefault -- this is the default
216+
, flagFormatText
217+
]
218+
```
219+
220+
# Consequences
221+
222+
### ✅ Positive
223+
224+
* *Declarative defaults*: Defaults are visible in the CLI parser, not buried in runtime logic.
225+
* *Consistent user experience*: CLI behavior and help messages are aligned across commands.
226+
* *Reduced duplication*: The `Vary` mechanism allows composable subsets of choices.
227+
* *Easier testing and documentation*: Defaults are discoverable without running the command.
228+
* *Fewer bugs*: Centralized handling makes incorrect or conflicting defaults less likely.
229+
230+
### ⚠️ Negative
231+
232+
* *Migration effort*: Existing commands must be updated to the new `Vary` + `Flag` approach.
233+
* *Learning curve*: Contributors will need to understand how `Flag`, `Vary`, and `setDefault` work.
234+
* *Breaking changes*: Some commands may behave differently after adopting this change.

0 commit comments

Comments
 (0)