-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommand.mojo
More file actions
4715 lines (4230 loc) · 185 KB
/
command.mojo
File metadata and controls
4715 lines (4230 loc) · 185 KB
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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Defines a CLI command and performs argument parsing."""
from os import getenv
from os.path import exists as _path_exists
from sys import argv, exit, stderr
from .argument import Argument
from .parse_result import ParseResult
from .utils import (
_RESET,
_BOLD_UL,
_DEFAULT_HEADER_COLOR,
_DEFAULT_ARG_COLOR,
_DEFAULT_WARN_COLOR,
_DEFAULT_ERROR_COLOR,
_correct_cjk_punctuation,
_display_width,
_fullwidth_to_halfwidth,
_has_fullwidth_chars,
_looks_like_number,
_resolve_color,
_split_on_fullwidth_spaces,
_suggest_similar,
)
# ---------- module-level file reader (workaround for Mojo compiler issue) ----
# Placing `open()` inside a method of a struct that contains `List[Self]` causes
# the compiler to deadlock when built with `-D ASSERT=warn` or `-D ASSERT=all`.
# Moving the I/O into a free function avoids the trigger.
fn _read_file_content(filepath: String) raises -> String:
"""Reads and returns the entire contents of *filepath*."""
with open(filepath, "r") as f:
return f.read()
fn _expand_response_files(
raw_args: List[String],
prefix: String,
max_depth: Int,
cmd_name: String,
) raises -> List[String]:
"""Expands response-file tokens in the argument list.
Free function (not a Command method) to work around a Mojo compiler
deadlock when ``open()`` or complex I/O appears inside a method of a
struct that contains ``List[Self]`` and is compiled with
``-D ASSERT=all``.
"""
var expanded = List[String]()
var plen = len(prefix)
for idx in range(len(raw_args)):
var token = raw_args[idx]
# Preserve argv[0] (program name) verbatim — never expand it.
if idx == 0:
expanded.append(token)
continue
# Check for escape: doubled prefix → literal.
if (
len(token) > plen * 2 - 1
and String(token[: plen * 2]) == prefix + prefix
):
expanded.append(String(token[plen:]))
continue
if len(token) > plen and String(token[:plen]) == prefix:
var filepath = String(token[plen:])
_read_response_file(
filepath, expanded, 0, prefix, max_depth, cmd_name
)
else:
expanded.append(token)
return expanded^
fn _read_response_file(
filepath: String,
mut out_args: List[String],
depth: Int,
prefix: String,
max_depth: Int,
cmd_name: String,
) raises:
"""Reads a single response file and appends its arguments.
Free function (not a Command method) — see ``_expand_response_files``
docstring for rationale.
"""
if depth >= max_depth:
var msg = (
"Response file nesting too deep (max "
+ String(max_depth)
+ "): "
+ filepath
)
print("error: " + cmd_name + ": " + msg, file=stderr)
raise Error(msg)
if not _path_exists(filepath):
var msg = "Response file not found: " + filepath
print("error: " + cmd_name + ": " + msg, file=stderr)
raise Error(msg)
var plen = len(prefix)
var content = _read_file_content(filepath)
var lines = content.split("\n")
for li in range(len(lines)):
var line = String(String(lines[li]).strip())
if len(line) == 0 or line.startswith("#"):
continue
# Escape: doubled prefix → literal.
if (
len(line) > plen * 2 - 1
and String(line[: plen * 2]) == prefix + prefix
):
out_args.append(String(line[plen:]))
continue
# Recursive response file.
if len(line) > plen and String(line[:plen]) == prefix:
var nested_path = String(line[plen:])
_read_response_file(
nested_path, out_args, depth + 1, prefix, max_depth, cmd_name
)
else:
out_args.append(line)
struct Command(Copyable, Movable, Stringable, Writable):
"""Defines a CLI command prototype with its arguments and handles parsing.
Example:
```mojo
from argmojo import Command, Argument
var command = Command("myapp", "A sample application")
command.add_argument(Argument("verbose", help="Enable verbose output").long["verbose"]().short["v"]().flag())
var result = command.parse()
```
"""
# === Public fields ===
var name: String
"""The command name (typically the program name)."""
var description: String
"""A short description of the command, shown in help text."""
var version: String
"""Version string for --version output."""
var args: List[Argument]
"""Registered argument definitions."""
var subcommands: List[Command]
"""Registered subcommand definitions. Each is a full Command instance."""
# === Private fields ===
var _exclusive_groups: List[List[String]]
"""Groups of mutually exclusive argument names."""
var _required_groups: List[List[String]]
"""Groups of arguments that must be provided together."""
var _one_required_groups: List[List[String]]
"""Groups where at least one argument must be provided."""
var _conditional_reqs: List[List[String]]
"""Pairs [target, condition]: target is required when condition is present."""
var _implications: List[List[String]]
"""Pairs [trigger, implied]: when trigger is set, implied is auto-set."""
var _help_on_no_arguments: Bool
"""When True, show help and exit if no arguments are provided."""
var _header_color: String
"""ANSI code for section headers (Usage, Arguments, Options)."""
var _arg_color: String
"""ANSI code for option / argument names."""
var _warn_color: String
"""ANSI code for deprecation warning messages (default: orange)."""
var _error_color: String
"""ANSI code for parse error messages (default: red)."""
var _is_help_subcommand: Bool
"""True for the auto-inserted 'help' pseudo-subcommand.
Never set this manually; use ``add_subcommand()`` to register subcommands and
``disable_help_subcommand()`` to opt out.
"""
var _help_subcommand_enabled: Bool
"""When True (default), auto-insert a 'help' subcommand on first
``add_subcommand()`` call."""
var _allow_negative_numbers: Bool
"""When True, tokens matching negative-number format (-N, -N.N, -NeX)
are always treated as positional arguments.
When False (default), the same treatment applies automatically whenever
no registered short option uses a digit character (auto-detect).
Enable explicitly via ``allow_negative_numbers()`` when you have a digit
short option and still need negative-number literals to pass through."""
var _allow_positional_with_subcommands: Bool
"""When True, allows mixing positional arguments with subcommands.
By default (False), registering a positional arg on a Command that already
has subcommands (or vice versa) raises an Error at registration time.
Call ``allow_positional_with_subcommands()`` to opt in explicitly."""
var _completions_enabled: Bool
"""When True (default), a built-in completion trigger is active.
Call ``disable_default_completions()`` to opt out entirely."""
var _completions_name: String
"""The name used for the built-in completion trigger.
Defaults to ``"completions"`` → ``--completions <shell>``.
Change via ``completions_name()``."""
var _completions_is_subcommand: Bool
"""When True, the completion trigger is a subcommand instead of an
option. Default False → ``--completions``. Call
``completions_as_subcommand()`` to switch to ``myapp completions bash``."""
var _command_aliases: List[String]
"""Alternate names for this command when used as a subcommand.
Add entries via ``command_aliases()``. Aliases are matched during
subcommand dispatch and appear inline next to the primary name in
help (e.g., "clone, cl"), but are not shown as separate entries."""
var _tips: List[String]
"""User-defined tips shown at the bottom of the help message.
Add entries via ``add_tip()``. Each tip is printed on its own line
prefixed with the same bold ``Tip:`` label as the built-in hint."""
var _is_hidden: Bool
"""When True, this command is excluded from help output, shell
completions, and 'available commands' error lists, but remains
dispatchable by exact name or alias. Set via ``hidden()``."""
var _response_file_prefix: String
"""Character prefix that marks a response-file token (e.g. ``@``).
When a token starts with this prefix, the remainder is treated as a
file path. Each line of the file is inserted as a separate argument.
Set via ``response_file_prefix()``. Empty string means disabled."""
var _response_file_max_depth: Int
"""Maximum nesting depth for recursive response-file expansion.
Prevents infinite loops when file A references file B and vice versa."""
var _disable_fullwidth_correction: Bool
"""When True, disable automatic full-width → half-width character
correction on option tokens. By default (False), tokens starting
with ``-`` that contain fullwidth ASCII characters (``U+FF01``–
``U+FF5E``) or fullwidth spaces (``U+3000``) are auto-corrected
with a warning. Call ``disable_fullwidth_correction()`` to opt out."""
var _disable_punctuation_correction: Bool
"""When True, disable CJK punctuation detection in error recovery.
By default (False), when an unknown option is encountered, common
CJK punctuation (e.g. em-dash ``U+2014``) is substituted before
running Levenshtein typo suggestion. Call
``disable_punctuation_correction()`` to opt out."""
# ===------------------------------------------------------------------=== #
# Life cycle methods
# ===------------------------------------------------------------------=== #
fn __init__(
out self,
name: String,
description: String = "",
version: String = "0.1.0",
):
"""Creates a new Command.
Args:
name: The command name.
description: A short description for help text.
version: Version string.
"""
self.name = name
self.description = description
self.version = version
self.args = List[Argument]()
self.subcommands = List[Command]()
self._exclusive_groups = List[List[String]]()
self._required_groups = List[List[String]]()
self._one_required_groups = List[List[String]]()
self._conditional_reqs = List[List[String]]()
self._implications = List[List[String]]()
self._help_on_no_arguments = False
self._is_help_subcommand = False
self._help_subcommand_enabled = True
self._allow_negative_numbers = False
self._allow_positional_with_subcommands = False
self._completions_enabled = True
self._completions_name = String("completions")
self._completions_is_subcommand = False
self._command_aliases = List[String]()
self._tips = List[String]()
self._is_hidden = False
self._response_file_prefix = String("")
self._response_file_max_depth = 10
self._disable_fullwidth_correction = False
self._disable_punctuation_correction = False
self._header_color = _DEFAULT_HEADER_COLOR
self._arg_color = _DEFAULT_ARG_COLOR
self._warn_color = _DEFAULT_WARN_COLOR
self._error_color = _DEFAULT_ERROR_COLOR
fn __moveinit__(out self, deinit move: Self):
"""Moves a Command, transferring ownership of all fields.
Args:
move: The Command to move from.
"""
self.name = move.name^
self.description = move.description^
self.version = move.version^
self.args = move.args^
self.subcommands = move.subcommands^
self._exclusive_groups = move._exclusive_groups^
self._required_groups = move._required_groups^
self._one_required_groups = move._one_required_groups^
self._conditional_reqs = move._conditional_reqs^
self._implications = move._implications^
self._help_on_no_arguments = move._help_on_no_arguments
self._is_help_subcommand = move._is_help_subcommand
self._help_subcommand_enabled = move._help_subcommand_enabled
self._allow_negative_numbers = move._allow_negative_numbers
self._allow_positional_with_subcommands = (
move._allow_positional_with_subcommands
)
self._completions_enabled = move._completions_enabled
self._completions_name = move._completions_name^
self._completions_is_subcommand = move._completions_is_subcommand
self._command_aliases = move._command_aliases^
self._tips = move._tips^
self._is_hidden = move._is_hidden
self._response_file_prefix = move._response_file_prefix^
self._response_file_max_depth = move._response_file_max_depth
self._disable_fullwidth_correction = move._disable_fullwidth_correction
self._disable_punctuation_correction = (
move._disable_punctuation_correction
)
self._header_color = move._header_color^
self._arg_color = move._arg_color^
self._warn_color = move._warn_color^
self._error_color = move._error_color^
fn __copyinit__(out self, copy: Self):
"""Creates a deep copy of a Command.
All field data — including registered args and subcommands — is
duplicated. Builder-pattern usage with ``add_subcommand(sub^)``
moves rather than copies, so this is only triggered when a
``Command`` value is assigned via ``=``.
Args:
copy: The Command to copy from.
"""
self.name = copy.name
self.description = copy.description
self.version = copy.version
self.args = copy.args.copy()
self.subcommands = copy.subcommands.copy()
self._exclusive_groups = List[List[String]]()
for i in range(len(copy._exclusive_groups)):
self._exclusive_groups.append(copy._exclusive_groups[i].copy())
self._required_groups = List[List[String]]()
for i in range(len(copy._required_groups)):
self._required_groups.append(copy._required_groups[i].copy())
self._one_required_groups = List[List[String]]()
for i in range(len(copy._one_required_groups)):
self._one_required_groups.append(
copy._one_required_groups[i].copy()
)
self._conditional_reqs = List[List[String]]()
for i in range(len(copy._conditional_reqs)):
self._conditional_reqs.append(copy._conditional_reqs[i].copy())
self._implications = List[List[String]]()
for i in range(len(copy._implications)):
self._implications.append(copy._implications[i].copy())
self._help_on_no_arguments = copy._help_on_no_arguments
self._is_help_subcommand = copy._is_help_subcommand
self._help_subcommand_enabled = copy._help_subcommand_enabled
self._allow_negative_numbers = copy._allow_negative_numbers
self._allow_positional_with_subcommands = (
copy._allow_positional_with_subcommands
)
self._completions_enabled = copy._completions_enabled
self._completions_name = copy._completions_name
self._completions_is_subcommand = copy._completions_is_subcommand
self._command_aliases = copy._command_aliases.copy()
self._tips = copy._tips.copy()
self._is_hidden = copy._is_hidden
self._response_file_prefix = copy._response_file_prefix
self._response_file_max_depth = copy._response_file_max_depth
self._disable_fullwidth_correction = copy._disable_fullwidth_correction
self._disable_punctuation_correction = (
copy._disable_punctuation_correction
)
self._header_color = copy._header_color
self._arg_color = copy._arg_color
self._warn_color = copy._warn_color
self._error_color = copy._error_color
# ===------------------------------------------------------------------=== #
# Builder methods for configuring the command
# ===------------------------------------------------------------------=== #
fn add_argument(mut self, var argument: Argument) raises:
"""Registers an argument definition.
Raises:
Error if adding a positional argument to a Command that already
has subcommands registered, unless
``allow_positional_with_subcommands()`` has been called.
Args:
argument: The Argument to register.
Example:
```mojo
from argmojo import Command, Argument
var command = Command("myapp", "A sample application")
command.add_argument(Argument("verbose", help="Enable verbose output"))
var result = command.parse()
```
"""
# Guard: positional args + subcommands require explicit opt-in.
if (
argument._is_positional
and len(self.subcommands) > 0
and not self._allow_positional_with_subcommands
):
self._error(
"Cannot add positional argument '"
+ argument.name
+ "' to '"
+ self.name
+ "' which already has subcommands. Call"
" allow_positional_with_subcommands() to opt in"
)
# Guard: require_equals / default_if_no_value + multi-value is unsupported.
if (
argument._require_equals or argument._has_default_if_no_value
) and argument._number_of_values > 0:
self._error(
"Argument '"
+ argument.name
+ "': .require_equals() / .default_if_no_value() cannot be"
" combined with .number_of_values[N]() (multi-value options)"
)
# Guard: remainder must not be combined with long/short (it is positional-only).
if argument._is_remainder and (
argument._long_name or argument._short_name
):
self._error(
"Argument '"
+ argument.name
+ "': .remainder() is for positional arguments only; remove"
" .long() / .short()"
)
# Guard: at most one remainder positional is allowed.
if argument._is_remainder:
for _ri in range(len(self.args)):
if self.args[_ri]._is_remainder:
self._error(
"Argument '"
+ argument.name
+ "': only one .remainder() positional is allowed"
" (already set on '"
+ self.args[_ri].name
+ "')"
)
# Guard: no positional may be added after a remainder.
if argument._is_positional and not argument._is_remainder:
for _ri in range(len(self.args)):
if self.args[_ri]._is_remainder:
self._error(
"Argument '"
+ argument.name
+ "': cannot add a positional argument after"
" a .remainder() positional ('"
+ self.args[_ri].name
+ "')"
)
# Guard: .prompt() conflicts with help_on_no_arguments().
if argument._prompt and self._help_on_no_arguments:
self._error(
"Argument '"
+ argument.name
+ "': .prompt() cannot be used on a command with"
" help_on_no_arguments() — when no arguments are"
" provided, help is shown and prompting never runs."
" Remove help_on_no_arguments() or .prompt()"
)
self.args.append(argument^)
fn add_subcommand(mut self, var sub: Command) raises:
"""Registers a subcommand.
A subcommand is a full ``Command`` instance that handles a specific verb
(e.g. ``app search …``, ``app init …``). After parsing, the selected
subcommand name is stored in ``result.subcommand`` and its own parsed
values are available via ``result.get_subcommand_result()``.
Args:
sub: The subcommand ``Command`` to register.
Raises:
Error if a persistent argument on this command shares a ``long_name``
or ``short_name`` with any local argument on ``sub``.
Example:
```mojo
from argmojo import Command, Argument
var app = Command("app", "My CLI tool", version="0.3.0")
var search = Command("search", "Search for patterns")
search.add_argument(Argument("pattern", help="Search pattern").required().positional())
var init = Command("init", "Initialize a new project")
init.add_argument(Argument("name", help="Project name").required().positional())
app.add_subcommand(search^)
app.add_subcommand(init^)
```
"""
# Guard: subcommands + positional args require explicit opt-in.
if not self._allow_positional_with_subcommands:
for _pi in range(len(self.args)):
if self.args[_pi]._is_positional:
self._error(
"Cannot add subcommand '"
+ sub.name
+ "' to '"
+ self.name
+ "' which already has positional argument '"
+ self.args[_pi].name
+ "'. Call"
" allow_positional_with_subcommands() to opt in"
)
# Conflict check: persistent parent args must not share names with
# any local arg in the child — that would make the option ambiguous
# after injection.
for pi in range(len(self.args)):
if not self.args[pi]._is_persistent:
continue
var pa = self.args[pi].copy()
for ci in range(len(sub.args)):
var ca = sub.args[ci].copy()
if (
pa._long_name
and ca._long_name
and pa._long_name == ca._long_name
):
self._error(
"Persistent flag '--"
+ pa._long_name
+ "' on '"
+ self.name
+ "' conflicts with '--"
+ ca._long_name
+ "' on subcommand '"
+ sub.name
+ "'"
)
if (
pa._short_name
and ca._short_name
and pa._short_name == ca._short_name
):
self._error(
"Persistent flag '-"
+ pa._short_name
+ "' on '"
+ self.name
+ "' conflicts with '-"
+ ca._short_name
+ "' on subcommand '"
+ sub.name
+ "'"
)
# Auto-register the 'help' subcommand as the first entry once.
# This keeps help discoverable at a fixed position (index 0) while
# user-defined subcommands remain in registration order after it.
# Disabled via disable_help_subcommand(); guard prevents duplicates.
if self._help_subcommand_enabled and self._find_subcommand("help") < 0:
var h = Command("help", "Show help for a subcommand")
h._is_help_subcommand = True
self.subcommands.append(h^)
self.subcommands.append(sub^)
fn disable_help_subcommand(mut self):
"""Opts out of the auto-added ``help`` subcommand.
By default, the first call to ``add_subcommand()`` automatically
registers a ``help`` subcommand so that ``app help <sub>`` works as
an alias for ``app <sub> --help``.
Call this before or after ``add_subcommand()`` to suppress the
feature — useful when ``"help"`` is a legitimate first positional
value (e.g. a search pattern or entity name). After disabling, use
``app <sub> --help`` directly.
Example:
```mojo
from argmojo import Command
var app = Command("search", "Search engine")
app.disable_help_subcommand() # "help" is a valid search query
# Now: ``search help init`` → positionals ["help", "init"] on root,
# so that you can do something like: search "help" in path "init".
# ``search init --help`` → shows init's help page
```
"""
self._help_subcommand_enabled = False
# Remove any already-inserted help subcommand.
var new_subs = List[Command]()
for i in range(len(self.subcommands)):
if not self.subcommands[i]._is_help_subcommand:
new_subs.append(self.subcommands[i].copy())
self.subcommands = new_subs^
fn allow_negative_numbers(mut self):
"""Treats tokens that look like negative numbers as positional arguments.
By default ArgMojo already auto-detects negative-number tokens
(``-9``, ``-3.14``, ``-1.5e10``) and passes them through as
positionals **when no registered short option starts with a digit**.
Call this method explicitly when you have registered a digit short
option (e.g., ``-3`` for ``--triple``) and still need negative-number
literals to be treated as positionals.
Example:
```mojo
from argmojo import Command, Argument
var command = Command("calc", "Calculator")
command.allow_negative_numbers()
command.add_argument(Argument("expr", help="Expression").positional().required())
# Now: calc -10.18 → positionals = ["-10.18"]
```
"""
self._allow_negative_numbers = True
fn allow_positional_with_subcommands(mut self):
"""Allows a Command to have both positional args and subcommands.
By default, ArgMojo follows the convention of cobra (Go) and clap
(Rust): a Command with subcommands cannot also have positional
arguments, because the parser cannot unambiguously distinguish a
subcommand name from a positional value.
Call this method **before** registering positional args and
subcommands to opt in to the mixed mode. In mixed mode, a token
that exactly matches a registered subcommand name is dispatched;
any other token falls through to the positional list.
Example:
```mojo
from argmojo import Command, Argument
var app = Command("tool", "Flexible tool")
app.allow_positional_with_subcommands()
app.add_argument(Argument("target", help="Default target").positional())
var sub = Command("build", "Build the project")
app.add_subcommand(sub^)
# Now: tool build → dispatch to 'build' subcommand
# tool foo.txt → positional "foo.txt"
```
"""
self._allow_positional_with_subcommands = True
fn disable_default_completions(mut self):
"""Disables the built-in completion trigger entirely.
By default, every ``Command`` has a built-in ``--completions bash``
(or ``zsh`` / ``fish``) that prints a shell completion script and
exits. Call this method to remove that trigger completely.
The ``generate_completion()`` method is still available for
programmatic use — only the automatic trigger is removed.
Example:
```mojo
from argmojo import Command
var app = Command("myapp", "My CLI")
app.disable_default_completions()
# --completions is now an unknown option
# but app.generate_completion["bash"]() still works
```
"""
self._completions_enabled = False
fn completions_name(mut self, name: String):
"""Sets the name used for the built-in completion trigger.
Default is ``"completions"`` → ``--completions <shell>``.
Change to any name you prefer:
- ``app.completions_name("autocomp")`` → ``--autocomp bash``
- ``app.completions_name("generate-completions")`` → ``--generate-completions bash``
Combine with ``completions_as_subcommand()`` to use as a subcommand:
- ``app.completions_name("comp")`` + ``app.completions_as_subcommand()``
→ ``myapp comp bash``
Args:
name: The new trigger name (without ``--`` prefix).
Example:
```mojo
from argmojo import Command
var app = Command("myapp", "My CLI")
app.completions_name("autocomp")
# Now: myapp --autocomp bash
```
"""
self._completions_name = name
fn completions_as_subcommand(mut self):
"""Switches the built-in completion trigger from an option to a subcommand.
Default behaviour: ``myapp --completions bash``
After calling this: ``myapp completions bash``
Combine with ``completions_name()`` to customise the subcommand name:
```mojo
from argmojo import Command
var app = Command("decimo", "CLI calculator based on decimo")
app.completions_name("comp")
app.completions_as_subcommand()
# → myapp comp bash
```
The subcommand is auto-registered when ``parse()`` runs. It does
**not** appear in help output by default (like the ``help``
subcommand). The auto-registered subcommand takes one positional
argument (the shell name) and handles printing + exiting.
"""
self._completions_is_subcommand = True
fn add_tip(mut self, tip: String):
"""Adds a custom tip line to the bottom of the help message.
Each tip is printed on its own line below the built-in ``--``
separator hint, prefixed with a bold ``Tip:`` label. Useful for
documenting shell idioms, environment variables, or any other
usage notes that don't fit in argument help strings.
Args:
tip: The tip text to display.
Example:
```mojo
from argmojo import Command, Argument
var command = Command("myapp", "A sample application")
command.add_tip("Set MYAPP_DEBUG=1 to enable debug logging.")
command.add_tip("Config file: ~/.config/myapp/config.toml")
```
"""
self._tips.append(tip)
# TODO: alias_name[name: StringLiteral](mut self) for compile-time checks
fn command_aliases(mut self, var names: List[String]):
"""Registers alternate names for this command when used as a subcommand.
Aliases are matched during subcommand dispatch and included in
shell completion scripts, but they do **not** appear as separate
entries in the ``Commands:`` help section. Instead, aliases are
shown inline next to the primary name.
Args:
names: The list of alias strings.
Example:
```mojo
from argmojo import Command
var clone = Command("clone", "Clone a repository")
var aliases: List[String] = ["cl"]
clone.command_aliases(aliases^)
# Now: mgit cl ... is equivalent to mgit clone ...
```
"""
for i in range(len(names)):
self._command_aliases.append(names[i])
fn hidden(mut self):
"""Marks this subcommand as hidden.
A hidden subcommand is excluded from help output, shell completion
scripts, and the "Available commands" error message, but remains
dispatchable by exact name or alias. Useful for internal,
experimental, or deprecated subcommands.
Example:
```mojo
from argmojo import Command
var app = Command("myapp", "A sample application")
var debug = Command("debug", "Internal debug command")
debug.hidden()
app.add_subcommand(debug^)
# 'debug' won't appear in --help or completions, but:
# app debug ... still works
```
"""
self._is_hidden = True
# TODO: response_file_prefix[prefix: StringLiteral](mut self) for compile-time checks
fn response_file_prefix(mut self, prefix: String = "@"):
"""Enables response-file expansion for this command.
Warning: **Temporarily disabled** — the underlying expansion
logic is compiled out to work around a Mojo compiler deadlock
triggered by ``-D ASSERT=all``. Calling this method still
stores the prefix, but ``parse_arguments()`` will **not**
expand response-file tokens until the compiler bug is fixed.
When enabled, any token that starts with the given ``prefix``
is treated as a response-file reference. The remainder of
the token is the file path; each non-empty, non-comment line
of that file is inserted as a separate argument in place of
the original token.
- Blank lines and lines starting with ``#`` are ignored.
- Leading / trailing whitespace on each line is stripped.
- Response files may reference other response files (recursive),
up to the configured nesting depth (set via
``response_file_max_depth[depth]()``; default 10).
- To pass a literal token that starts with the prefix (e.g. an
email ``@user``), escape it by doubling the prefix: ``@@user``
is inserted as ``@user``.
Args:
prefix: The prefix character(s) that introduce a response
file (default ``"@"``).
Example:
```mojo
from argmojo import Command
var command = Command("myapp", "A sample application")
command.response_file_prefix() # uses default '@'
# Now: myapp @args.txt reads arguments from args.txt
```
"""
self._response_file_prefix = prefix
fn response_file_max_depth[depth: Int](mut self) where depth > 0:
"""Sets the maximum nesting depth for response-file expansion.
Warning: **Temporarily disabled** — see
``response_file_prefix()`` docstring for details.
Parameters:
depth: Maximum recursion depth (default 10).
Constraints: must be positive.
"""
self._response_file_max_depth = depth
fn disable_fullwidth_correction(mut self):
"""Disables automatic full-width → half-width character correction.
By default, ArgMojo detects fullwidth ASCII characters
(``U+FF01``–``U+FF5E``) and fullwidth spaces (``U+3000``) in
option tokens (those starting with ``-``) and auto-corrects them
to their halfwidth equivalents with a warning. This helps CJK
users who forget to switch input methods.
Call this method to disable that correction entirely — useful
when strict parsing is preferred.
Example:
```mojo
from argmojo import Command
var app = Command("myapp", "My CLI")
app.disable_fullwidth_correction()
# Now: --verbose is NOT corrected → unknown option error
```
"""
self._disable_fullwidth_correction = True
fn disable_punctuation_correction(mut self):
"""Disables CJK punctuation detection in error recovery.
By default, when an unknown option is encountered, ArgMojo tries
substituting common CJK punctuation (e.g. em-dash ``——`` →
``--``) before running Levenshtein typo suggestion. This helps
CJK users who accidentally type Chinese punctuation.
Call this method to disable that behaviour — useful when strict
error messages are preferred.
Example:
```mojo
from argmojo import Command
var app = Command("myapp", "My CLI")
app.disable_punctuation_correction()
# Now: ——verbose will NOT attempt em-dash → hyphen correction
```
"""
self._disable_punctuation_correction = True
fn mutually_exclusive(mut self, var names: List[String]) raises:
"""Declares a group of mutually exclusive arguments.
At most one argument from each group may be provided. Parsing
will fail if more than one is present.
All names must refer to arguments already registered via
``add_argument()``. An ``Error`` is raised immediately if any
name is unknown, so that typos are caught during command
construction rather than at end-user runtime.
Args:
names: The internal names of the arguments in the group.
Raises:
Error: If any name in the group is not a registered argument.
Example:
```mojo
from argmojo import Command, Argument
var command = Command("myapp", "A sample application")
command.add_argument(Argument("json", help="Output as JSON").long["json"]().flag())
command.add_argument(Argument("yaml", help="Output as YAML").long["yaml"]().flag())
command.mutually_exclusive(["json", "yaml"])
```
"""
if len(names) == 0:
raise Error("mutually_exclusive(): 'names' list must not be empty")
var unique = List[String]()
for ni in range(len(names)):
var dup = False
for ui in range(len(unique)):
if unique[ui] == names[ni]:
dup = True
break
if dup:
continue # Proceed to the next name; duplicates are ignored.
var found = False
for ai in range(len(self.args)):
if self.args[ai].name == names[ni]:
found = True
break
if not found:
raise Error(
"mutually_exclusive(): unknown argument '" + names[ni] + "'"
)
unique.append(names[ni])
self._exclusive_groups.append(unique^)
fn required_together(mut self, var names: List[String]) raises:
"""Declares a group of arguments that must be provided together.
If any argument from the group is provided, all others in the
group must also be provided. Parsing will fail otherwise.
All names must refer to arguments already registered via
``add_argument()``. An ``Error`` is raised immediately if any
name is unknown, so that typos are caught during command
construction rather than at end-user runtime.
Args:
names: The internal names of the arguments in the group.
Raises:
Error: If any name in the group is not a registered argument.
Example:
```mojo
from argmojo import Command, Argument
var command = Command("myapp", "A sample application")
command.add_argument(Argument("username", help="Auth username").long["username"]().short["u"]())
command.add_argument(Argument("password", help="Auth password").long["password"]().short["p"]())
command.required_together(["username", "password"])
```
"""
if len(names) == 0:
raise Error("required_together(): 'names' list must not be empty")
var unique = List[String]()
for ni in range(len(names)):
var dup = False
for ui in range(len(unique)):
if unique[ui] == names[ni]:
dup = True
break
if dup:
continue
var found = False
for ai in range(len(self.args)):
if self.args[ai].name == names[ni]:
found = True
break
if not found:
raise Error(
"required_together(): unknown argument '" + names[ni] + "'"
)
unique.append(names[ni])
self._required_groups.append(unique^)