|
| 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