deno_cli_parser 0.5.0

Zero-cost CLI argument parser for the Deno CLI
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
// Copyright 2018-2026 the Deno authors. MIT license.
use crate::error::CliError;
use crate::error::CliErrorKind;

/// Definition of a CLI command (root or subcommand).
/// All fields are `const`-constructible - no heap allocation.
#[derive(Debug, Clone, Copy)]
pub struct CommandDef {
  pub name: &'static str,
  pub about: &'static str,
  pub aliases: &'static [&'static str],
  pub args: &'static [ArgDef],
  /// Arg groups that share arguments across commands (e.g. permission args,
  /// compile args). These are flattened into `args` during parsing.
  pub arg_groups: &'static [&'static [ArgDef]],
  pub subcommands: &'static [CommandDef],
  /// When no subcommand name matches argv, use this subcommand's args.
  pub default_subcommand: Option<&'static str>,
  /// If true, once the first positional arg is consumed, all remaining
  /// args are treated as positional (no flag parsing).
  pub trailing_var_arg: bool,
  /// If true, pass all remaining args after subcommand name through
  /// without parsing (deploy/sandbox pattern).
  pub passthrough: bool,
}

impl CommandDef {
  pub const fn new(name: &'static str) -> Self {
    Self {
      name,
      about: "",
      aliases: &[],
      args: &[],
      arg_groups: &[],
      subcommands: &[],
      default_subcommand: None,
      trailing_var_arg: false,
      passthrough: false,
    }
  }

  /// Find a subcommand by name or alias.
  pub fn find_subcommand(&self, name: &str) -> Option<&CommandDef> {
    self
      .subcommands
      .iter()
      .find(|cmd| cmd.name == name || cmd.aliases.contains(&name))
  }

  /// Iterate over all args: own args + all arg group args.
  pub fn all_args(&self) -> impl Iterator<Item = &ArgDef> {
    self
      .args
      .iter()
      .chain(self.arg_groups.iter().flat_map(|g| g.iter()))
  }

  /// Find an arg by long name.
  pub fn find_arg_long(&self, long: &str) -> Option<&ArgDef> {
    self
      .all_args()
      .find(|a| a.long == Some(long) || a.long_aliases.contains(&long))
  }

  /// Find an arg by short name.
  pub fn find_arg_short(&self, short: char) -> Option<&ArgDef> {
    self
      .all_args()
      .find(|a| a.short == Some(short) || a.short_aliases.contains(&short))
  }
}

/// Definition of a single CLI argument/flag.
#[derive(Debug, Clone, Copy)]
pub struct ArgDef {
  /// Internal identifier for this arg (used to retrieve parsed values).
  pub name: &'static str,
  pub short: Option<char>,
  pub long: Option<&'static str>,
  pub short_aliases: &'static [char],
  pub long_aliases: &'static [&'static str],
  pub help: &'static str,
  pub action: ArgAction,
  pub num_args: NumArgs,
  /// If set, values like `--flag=a,b,c` are split on this char.
  pub value_delimiter: Option<char>,
  /// If true, value must be attached with `=` (e.g. `--flag=val`).
  pub require_equals: bool,
  pub required: bool,
  pub default_value: Option<&'static str>,
  pub global: bool,
  pub hidden: bool,
  /// If true, this is a positional argument (no `--`/`-` prefix).
  pub positional: bool,
  /// If true (and positional), once this positional starts consuming values,
  /// ALL remaining args (including flags like `--foo`) are collected as
  /// values for this positional. This implements per-positional
  /// `trailing_var_arg` behavior from clap.
  pub trailing: bool,
  /// Hint for shell completions.
  pub value_name: Option<&'static str>,
  /// Names of args that cannot be used together with this one. If both are
  /// present, parsing fails (mirrors clap's `conflicts_with`).
  pub conflicts: &'static [&'static str],
  /// Names of args that must also be present when this one is used. If any
  /// is missing, parsing fails (mirrors clap's `requires`).
  pub requires: &'static [&'static str],
  /// Optional declarative validation for each value (mirrors clap's
  /// `value_parser`). Invalid values produce a parse error.
  pub value_parser: Option<ValueParser>,
}

impl ArgDef {
  pub const fn new(name: &'static str) -> Self {
    Self {
      name,
      short: None,
      long: None,
      short_aliases: &[],
      long_aliases: &[],
      help: "",
      action: ArgAction::Set,
      num_args: NumArgs::Exact(1),
      value_delimiter: None,
      require_equals: false,
      required: false,
      default_value: None,
      global: false,
      hidden: false,
      positional: false,
      trailing: false,
      value_name: None,
      conflicts: &[],
      requires: &[],
      value_parser: None,
    }
  }

  pub const fn value_parser(mut self, parser: ValueParser) -> Self {
    self.value_parser = Some(parser);
    self
  }

  pub const fn conflicts_with(
    mut self,
    conflicts: &'static [&'static str],
  ) -> Self {
    self.conflicts = conflicts;
    self
  }

  pub const fn requires(mut self, requires: &'static [&'static str]) -> Self {
    self.requires = requires;
    self
  }

  pub const fn long(mut self, long: &'static str) -> Self {
    self.long = Some(long);
    self
  }

  pub const fn short(mut self, short: char) -> Self {
    self.short = Some(short);
    self
  }

  pub const fn action(mut self, action: ArgAction) -> Self {
    self.action = action;
    self
  }

  pub const fn num_args(mut self, num_args: NumArgs) -> Self {
    self.num_args = num_args;
    self
  }

  pub const fn set_true(mut self) -> Self {
    self.action = ArgAction::SetTrue;
    self.num_args = NumArgs::Exact(0);
    self
  }

  pub const fn positional(mut self) -> Self {
    self.positional = true;
    self
  }

  pub const fn required(mut self) -> Self {
    self.required = true;
    self
  }

  pub const fn global(mut self) -> Self {
    self.global = true;
    self
  }

  pub const fn value_delimiter(mut self, delim: char) -> Self {
    self.value_delimiter = Some(delim);
    self
  }

  pub const fn require_equals(mut self) -> Self {
    self.require_equals = true;
    self
  }

  pub const fn hidden(mut self) -> Self {
    self.hidden = true;
    self
  }

  pub const fn default_value(mut self, val: &'static str) -> Self {
    self.default_value = Some(val);
    self
  }

  pub const fn help(mut self, help: &'static str) -> Self {
    self.help = help;
    self
  }

  pub const fn short_aliases(mut self, aliases: &'static [char]) -> Self {
    self.short_aliases = aliases;
    self
  }

  pub const fn long_aliases(
    mut self,
    aliases: &'static [&'static str],
  ) -> Self {
    self.long_aliases = aliases;
    self
  }

  pub const fn trailing(mut self) -> Self {
    self.trailing = true;
    self
  }

  pub const fn value_name(mut self, name: &'static str) -> Self {
    self.value_name = Some(name);
    self
  }
}

/// What the parser does when it encounters this argument.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArgAction {
  /// Boolean flag, sets to true when present.
  SetTrue,
  /// Takes a single value, last one wins.
  Set,
  /// Collects all occurrences into a Vec.
  Append,
  /// Counts occurrences (e.g. -vvv -> 3).
  Count,
}

/// How many values an argument takes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumArgs {
  /// Exactly N values.
  Exact(usize),
  /// 0 or 1 value (optional value).
  Optional,
  /// 0 or more values.
  ZeroOrMore,
  /// 1 or more values.
  OneOrMore,
}

/// Declarative value validation, mirroring clap's `value_parser`. Applied to
/// each value as it is parsed; an invalid value produces a parse error instead
/// of being silently accepted or coerced downstream.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueParser {
  /// One of a fixed set of allowed strings.
  Choices(&'static [&'static str]),
  /// A boolean (`true`/`false`).
  Bool,
  /// An unsigned integer of the given width.
  U8,
  U16,
  U32,
  U64,
  Usize,
  /// A non-zero unsigned integer of the given width.
  NonZeroU8,
  NonZeroU32,
  NonZeroUsize,
  /// A u32 within an inclusive range (e.g. a percentage 0..=100).
  U32Range(u32, u32),
}

impl ValueParser {
  /// Validate `value`, returning a parse error on failure. `flag` is the
  /// user-facing flag/arg name used in the error message.
  pub fn validate(&self, value: &str, flag: &str) -> Result<(), CliError> {
    let ok = match self {
      ValueParser::Choices(choices) => choices.contains(&value),
      ValueParser::Bool => value.parse::<bool>().is_ok(),
      ValueParser::U8 => value.parse::<u8>().is_ok(),
      ValueParser::U16 => value.parse::<u16>().is_ok(),
      ValueParser::U32 => value.parse::<u32>().is_ok(),
      ValueParser::U64 => value.parse::<u64>().is_ok(),
      ValueParser::Usize => value.parse::<usize>().is_ok(),
      ValueParser::NonZeroU8 => value.parse::<std::num::NonZeroU8>().is_ok(),
      ValueParser::NonZeroU32 => value.parse::<std::num::NonZeroU32>().is_ok(),
      ValueParser::NonZeroUsize => {
        value.parse::<std::num::NonZeroUsize>().is_ok()
      }
      ValueParser::U32Range(lo, hi) => {
        value.parse::<u32>().is_ok_and(|n| n >= *lo && n <= *hi)
      }
    };
    if ok {
      return Ok(());
    }
    let message = match self {
      ValueParser::Choices(choices) => format!(
        "invalid value '{value}' for '{flag}': possible values: {}",
        choices.join(", ")
      ),
      ValueParser::U32Range(lo, hi) => format!(
        "invalid value '{value}' for '{flag}': must be in range {lo}..={hi}"
      ),
      _ => format!("invalid value '{value}' for '{flag}'"),
    };
    Err(CliError::new(CliErrorKind::InvalidValue, message))
  }
}

/// Result of parsing: raw parsed data before conversion to typed Flags.
#[derive(Debug, Clone)]
pub struct ParseResult {
  /// Which subcommand was matched (None if default/root).
  pub subcommand: Option<String>,
  /// Parsed argument values, keyed by arg name.
  pub args: Vec<ParsedArg>,
  /// Remaining positional args after `--` or trailing var args.
  pub trailing: Vec<String>,
}

impl ParseResult {
  pub fn new() -> Self {
    Self {
      subcommand: None,
      args: Vec::new(),
      trailing: Vec::new(),
    }
  }

  /// Check if a boolean flag was set.
  pub fn get_bool(&self, name: &str) -> bool {
    self.args.iter().any(|a| a.name == name && a.is_present)
  }

  /// Get a single string value for an arg.
  pub fn get_one(&self, name: &str) -> Option<&str> {
    self
      .args
      .iter()
      .find(|a| a.name == name)
      .and_then(|a| a.values.first())
      .map(|s| s.as_str())
  }

  /// Get all values for an arg (for Append actions or multi-value args).
  pub fn get_many(&self, name: &str) -> Option<&[String]> {
    self
      .args
      .iter()
      .find(|a| a.name == name)
      .filter(|a| a.is_present)
      .map(|a| a.values.as_slice())
  }

  /// Check if an arg was explicitly provided on the command line.
  pub fn contains(&self, name: &str) -> bool {
    self.args.iter().any(|a| a.name == name && a.is_present)
  }

  /// Get the count for a Count action arg.
  pub fn get_count(&self, name: &str) -> usize {
    self
      .args
      .iter()
      .find(|a| a.name == name)
      .map(|a| a.count)
      .unwrap_or(0)
  }
}

impl Default for ParseResult {
  fn default() -> Self {
    Self::new()
  }
}

/// A single parsed argument with its values.
#[derive(Debug, Clone)]
pub struct ParsedArg {
  pub name: &'static str,
  pub values: Vec<String>,
  pub is_present: bool,
  pub count: usize,
}

impl ParsedArg {
  pub fn new(name: &'static str) -> Self {
    Self {
      name,
      values: Vec::new(),
      is_present: false,
      count: 0,
    }
  }
}