execsql2 2.1.2__py3-none-any.whl → 2.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +13 -1
  6. execsql/db/access.py +16 -12
  7. execsql/db/base.py +158 -90
  8. execsql/db/dsn.py +6 -5
  9. execsql/db/duckdb.py +2 -2
  10. execsql/db/firebird.py +23 -19
  11. execsql/db/mysql.py +8 -7
  12. execsql/db/oracle.py +11 -11
  13. execsql/db/postgres.py +28 -16
  14. execsql/db/sqlite.py +12 -11
  15. execsql/db/sqlserver.py +5 -3
  16. execsql/exceptions.py +7 -7
  17. execsql/exporters/base.py +6 -1
  18. execsql/exporters/delimited.py +44 -35
  19. execsql/exporters/duckdb.py +2 -2
  20. execsql/exporters/feather.py +6 -6
  21. execsql/exporters/html.py +83 -69
  22. execsql/exporters/json.py +50 -42
  23. execsql/exporters/latex.py +33 -27
  24. execsql/exporters/ods.py +4 -4
  25. execsql/exporters/parquet.py +2 -2
  26. execsql/exporters/pretty.py +11 -9
  27. execsql/exporters/raw.py +17 -13
  28. execsql/exporters/sqlite.py +2 -2
  29. execsql/exporters/templates.py +23 -15
  30. execsql/exporters/values.py +22 -20
  31. execsql/exporters/xls.py +4 -4
  32. execsql/exporters/xml.py +28 -13
  33. execsql/importers/base.py +4 -4
  34. execsql/importers/csv.py +6 -6
  35. execsql/importers/feather.py +4 -4
  36. execsql/importers/ods.py +4 -4
  37. execsql/importers/xls.py +4 -4
  38. execsql/metacommands/__init__.py +518 -67
  39. execsql/metacommands/conditions.py +101 -27
  40. execsql/metacommands/control.py +8 -4
  41. execsql/metacommands/data.py +6 -6
  42. execsql/metacommands/debug.py +6 -2
  43. execsql/metacommands/io.py +67 -1310
  44. execsql/metacommands/io_export.py +442 -0
  45. execsql/metacommands/io_fileops.py +287 -0
  46. execsql/metacommands/io_import.py +398 -0
  47. execsql/metacommands/io_write.py +248 -0
  48. execsql/metacommands/prompt.py +22 -66
  49. execsql/metacommands/system.py +7 -2
  50. execsql/py.typed +0 -0
  51. execsql/script.py +49 -5
  52. execsql/types.py +20 -20
  53. execsql/utils/fileio.py +15 -8
  54. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/METADATA +6 -6
  55. execsql2-2.2.1.dist-info/RECORD +104 -0
  56. execsql2-2.1.2.dist-info/RECORD +0 -96
  57. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/READ_ME.rst +0 -0
  58. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  59. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  60. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/execsql.conf +0 -0
  61. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  62. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  63. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  64. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  65. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  66. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  67. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  68. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/script_template.sql +0 -0
  69. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  70. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  71. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  72. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/WHEEL +0 -0
  73. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/entry_points.txt +0 -0
  74. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/LICENSE.txt +0 -0
  75. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/NOTICE +0 -0
@@ -29,7 +29,7 @@ from execsql.metacommands.connect import (
29
29
  x_connect_user_ora,
30
30
  x_connect_duckdb,
31
31
  x_connect_sqlite,
32
- x_connect_dsn, # noqa: F401
32
+ x_connect_dsn,
33
33
  x_use,
34
34
  x_disconnect,
35
35
  x_autocommit_on,
@@ -118,6 +118,7 @@ from execsql.metacommands.io import (
118
118
  x_import_parquet,
119
119
  x_import_feather,
120
120
  x_import_row_buffer,
121
+ x_show_progress,
121
122
  x_export_row_buffer,
122
123
  x_write,
123
124
  x_write_create_table,
@@ -173,6 +174,7 @@ from execsql.metacommands.system import (
173
174
  x_log,
174
175
  x_logwritemessages,
175
176
  x_log_datavars,
177
+ x_log_sql,
176
178
  x_console,
177
179
  x_consoleprogress,
178
180
  x_consolewait,
@@ -212,6 +214,41 @@ from execsql.utils.regex import (
212
214
  )
213
215
  from execsql.script import MetaCommandList
214
216
 
217
+ # ---------------------------------------------------------------------------
218
+ # Export format constants — single source of truth.
219
+ # Used in dispatch table regex patterns and by io_export.py for validation.
220
+ # ---------------------------------------------------------------------------
221
+ DELIMITED_FORMATS = ["CSV", "TAB", "TSV", "TABQ", "TSVQ", "UNITSEP", "US"]
222
+ TEXT_FORMATS = ["TXT", "TXT-AND", "PLAIN"]
223
+ JSON_VARIANT_FORMATS = ["JSON_TS", "JSON_TABLESCHEMA"]
224
+
225
+ QUERY_EXPORT_FORMATS = (
226
+ DELIMITED_FORMATS + TEXT_FORMATS + ["ODS", "JSON", "HTML", "CGI-HTML", "VALUES", "LATEX", "RAW", "B64", "FEATHER"]
227
+ )
228
+ TABLE_EXPORT_FORMATS = (
229
+ DELIMITED_FORMATS
230
+ + TEXT_FORMATS
231
+ + ["JSON", "XML", "VALUES", "HTML", "CGI-HTML", "SQLITE", "DUCKDB", "LATEX", "RAW", "B64", "FEATHER", "HDF5"]
232
+ )
233
+ SERVE_FORMATS = ["BINARY", "CSV", "TXT", "TEXT", "ODS", "JSON", "HTML", "PDF", "ZIP"]
234
+ METADATA_FORMATS = ["CSV", "TAB", "TSV", "TABQ", "TSVQ", "TXT", "TEXT"]
235
+ ALL_EXPORT_FORMATS = sorted(
236
+ set(QUERY_EXPORT_FORMATS + TABLE_EXPORT_FORMATS + JSON_VARIANT_FORMATS),
237
+ )
238
+
239
+ DATABASE_TYPES = [
240
+ "POSTGRESQL",
241
+ "MYSQL",
242
+ "MARIADB",
243
+ "ORACLE",
244
+ "SQLSERVER",
245
+ "FIREBIRD",
246
+ "ACCESS",
247
+ "DUCKDB",
248
+ "SQLITE",
249
+ "DSN",
250
+ ]
251
+
215
252
 
216
253
  def build_dispatch_table() -> MetaCommandList:
217
254
  """Construct and return the complete metacommand dispatch table."""
@@ -223,6 +260,8 @@ def build_dispatch_table() -> MetaCommandList:
223
260
  mcl.add(
224
261
  ins_fn_rxs(r"^\s*DEBUG\s+WRITE\s+METACOMMANDLIST\s+TO\s+", r"\s*$"),
225
262
  x_debug_write_metacommands,
263
+ description="DEBUG",
264
+ category="action",
226
265
  )
227
266
  mcl.add(r"^\s*DEBUG\s+WRITE\s+COMMANDLISTSTACK\s*$", x_debug_commandliststack)
228
267
  mcl.add(r"^\s*DEBUG\s+WRITE\s+IFLEVELS\s*$", x_debug_iflevels)
@@ -260,17 +299,29 @@ def build_dispatch_table() -> MetaCommandList:
260
299
  mcl.add(
261
300
  ins_fn_rxs(
262
301
  r"^\s*SERVE\s+",
263
- r"\s+AS\s+(?P<format>BINARY|CSV|TXT|TEXT|ODS|JSON|HTML|PDF|ZIP)\s*$",
302
+ rf"\s+AS\s+(?P<format>{'|'.join(SERVE_FORMATS)})\s*$",
264
303
  ),
265
304
  x_serve,
305
+ description="SERVE",
306
+ category="action",
266
307
  )
267
308
 
268
309
  # ------------------------------------------------------------------
269
310
  # Misc short commands
270
311
  # ------------------------------------------------------------------
271
- mcl.add(r"^\s*RESET\s+DIALOG_CANCELED\s*$", x_reset_dialog_canceled)
272
- mcl.add(r"^\s*SUB_QUERYSTRING\s+(?P<qstr>.+)\s*$", x_sub_querystring)
273
- mcl.add(r"^\s*BREAK\s*$", x_break)
312
+ mcl.add(
313
+ r"^\s*RESET\s+DIALOG_CANCELED\s*$",
314
+ x_reset_dialog_canceled,
315
+ description="RESET DIALOG_CANCELED",
316
+ category="action",
317
+ )
318
+ mcl.add(
319
+ r"^\s*SUB_QUERYSTRING\s+(?P<qstr>.+)\s*$",
320
+ x_sub_querystring,
321
+ description="SUB_QUERYSTRING",
322
+ category="action",
323
+ )
324
+ mcl.add(r"^\s*BREAK\s*$", x_break, description="BREAK", category="control")
274
325
 
275
326
  # ------------------------------------------------------------------
276
327
  # EXPORT QUERY (various formats)
@@ -280,22 +331,22 @@ def build_dispatch_table() -> MetaCommandList:
280
331
  r"^\s*EXPORT\s+QUERY\s+<<\s*(?P<query>.*;)\s*>>\s+(?P<tee>TEE\s+)?(?P<append>APPEND\s+)?TO\s+",
281
332
  ins_fn_rxs(
282
333
  r"(?:\s+IN\s+ZIPFILE\s+",
283
- r")?\s+AS\s*(?P<format>CSV|TAB|TSV|TABQ|TSVQ|UNITSEP|US|TXT|TXT-AND|PLAIN|ODS|JSON|"
284
- r'HTML|CGI-HTML|VALUES|LATEX|RAW|B64|FEATHER)(?:\s+DESCRIP(?:TION)?\s+"(?P<description>[^"]*)")?\s*$',
334
+ rf")?\s+AS\s*(?P<format>{'|'.join(QUERY_EXPORT_FORMATS)}|PARQUET|TXT-AND)"
335
+ r'(?:\s+DESCRIP(?:TION)?\s+"(?P<description>[^"]*)")?\s*$',
285
336
  symbolicname="zipfilename",
286
337
  ),
287
338
  ),
288
339
  x_export_query,
289
- "Write data from a query to a file.",
340
+ "EXPORT QUERY",
341
+ category="action",
290
342
  )
291
343
  mcl.add(
292
344
  ins_fn_rxs(
293
345
  r"^\s*EXPORT\s+QUERY\s+<<\s*(?P<query>.*;)\s*>>\s+(?P<tee>TEE\s+)?(?P<append>APPEND\s+)?TO\s+",
294
- r"\s+AS\s*(?P<format>JSON_TS|JSON_TABLESCHEMA)(?:\s+(?P<notype>NOTYPE))?"
346
+ rf"\s+AS\s*(?P<format>{'|'.join(JSON_VARIANT_FORMATS)})(?:\s+(?P<notype>NOTYPE))?"
295
347
  r'(?:\s+DESCRIP(?:TION)?\s+"(?P<description>[^"]*)")?\s*$',
296
348
  ),
297
349
  x_export_query,
298
- "Write data from a query to a file.",
299
350
  )
300
351
  mcl.add(
301
352
  ins_fn_rxs(
@@ -325,6 +376,8 @@ def build_dispatch_table() -> MetaCommandList:
325
376
  ),
326
377
  ),
327
378
  x_export_with_template,
379
+ description="EXPORT",
380
+ category="action",
328
381
  )
329
382
  mcl.add(
330
383
  ins_table_rxs(
@@ -334,14 +387,14 @@ def build_dispatch_table() -> MetaCommandList:
334
387
  r"\s+(?P<tee>TEE\s+)?(?P<append>APPEND\s+)?TO\s+",
335
388
  r"(?:\s+IN\s+ZIPFILE\s+",
336
389
  ),
337
- r")?\s+AS\s+(?P<format>CSV|TAB|TSV|TABQ|TSVQ|UNITSEP|US|TXT|TXT-AND|PLAIN|JSON|XML|"
338
- r"VALUES|HTML|CGI-HTML|SQLITE|DUCKDB|LATEX|RAW|B64|FEATHER|HDF5)"
390
+ rf")?\s+AS\s+(?P<format>{'|'.join(TABLE_EXPORT_FORMATS)}|PARQUET|TXT-AND)"
339
391
  r'(?:\s+DESCRIP(?:TION)?\s+"(?P<description>[^"]*)")?\s*$',
340
392
  symbolicname="zipfilename",
341
393
  ),
342
394
  ),
343
395
  x_export,
344
- "Write data from a table or view to a file.",
396
+ "EXPORT",
397
+ category="action",
345
398
  )
346
399
  mcl.add(
347
400
  ins_table_rxs(
@@ -351,13 +404,12 @@ def build_dispatch_table() -> MetaCommandList:
351
404
  r"\s+(?P<tee>TEE\s+)?(?P<append>APPEND\s+)?TO\s+",
352
405
  r"(?:\s+IN\s+ZIPFILE\s+",
353
406
  ),
354
- r")?\s+AS\s+(?P<format>JSON_TS|JSON_TABLESCHEMA)(?:\s+(?P<notype>NOTYPE))?"
407
+ rf")?\s+AS\s+(?P<format>{'|'.join(JSON_VARIANT_FORMATS)})(?:\s+(?P<notype>NOTYPE))?"
355
408
  r'(?:\s+DESCRIP(?:TION)?\s+"(?P<description>[^"]*)")?\s*$',
356
409
  symbolicname="zipfilename",
357
410
  ),
358
411
  ),
359
412
  x_export,
360
- "Write data from a table or view to a file.",
361
413
  )
362
414
  mcl.add(
363
415
  ins_table_list_rxs(
@@ -368,7 +420,6 @@ def build_dispatch_table() -> MetaCommandList:
368
420
  ),
369
421
  ),
370
422
  x_export_ods_multiple,
371
- "Write data from tables or views to an ODS file.",
372
423
  )
373
424
 
374
425
  # ------------------------------------------------------------------
@@ -383,6 +434,8 @@ def build_dispatch_table() -> MetaCommandList:
383
434
  ),
384
435
  ),
385
436
  x_import_file,
437
+ description="IMPORT_FILE",
438
+ category="action",
386
439
  )
387
440
  mcl.add(
388
441
  ins_table_rxs(
@@ -407,12 +460,14 @@ def build_dispatch_table() -> MetaCommandList:
407
460
  ),
408
461
  ),
409
462
  x_import_ods_pattern,
463
+ description="IMPORT",
464
+ category="action",
410
465
  )
411
466
 
412
467
  # ------------------------------------------------------------------
413
468
  # CD
414
469
  # ------------------------------------------------------------------
415
- mcl.add(r"^\s*CD\s+(?P<dir>.+)\s*$", x_cd)
470
+ mcl.add(r"^\s*CD\s+(?P<dir>.+)\s*$", x_cd, description="CD", category="action")
416
471
 
417
472
  # ------------------------------------------------------------------
418
473
  # IMPORT ODS (single sheet)
@@ -507,6 +562,8 @@ def build_dispatch_table() -> MetaCommandList:
507
562
  ),
508
563
  ),
509
564
  x_prompt_action,
565
+ description="PROMPT ACTION",
566
+ category="prompt",
510
567
  )
511
568
  mcl.add(
512
569
  ins_table_rxs(
@@ -533,6 +590,8 @@ def build_dispatch_table() -> MetaCommandList:
533
590
  symbolicname="startdir",
534
591
  ),
535
592
  x_prompt_savefile,
593
+ description="PROMPT SAVEFILE",
594
+ category="prompt",
536
595
  )
537
596
  mcl.add(
538
597
  ins_fn_rxs(
@@ -543,6 +602,8 @@ def build_dispatch_table() -> MetaCommandList:
543
602
  symbolicname="startdir",
544
603
  ),
545
604
  x_prompt_openfile,
605
+ description="PROMPT OPENFILE",
606
+ category="prompt",
546
607
  )
547
608
  mcl.add(
548
609
  ins_fn_rxs(
@@ -552,6 +613,8 @@ def build_dispatch_table() -> MetaCommandList:
552
613
  symbolicname="startdir",
553
614
  ),
554
615
  x_prompt_directory,
616
+ description="PROMPT DIRECTORY",
617
+ category="prompt",
555
618
  )
556
619
 
557
620
  # ------------------------------------------------------------------
@@ -568,6 +631,8 @@ def build_dispatch_table() -> MetaCommandList:
568
631
  suffix="1",
569
632
  ),
570
633
  x_prompt_select_rows,
634
+ description="PROMPT SELECT_ROWS",
635
+ category="prompt",
571
636
  )
572
637
  mcl.add(
573
638
  ins_table_rxs(
@@ -588,11 +653,21 @@ def build_dispatch_table() -> MetaCommandList:
588
653
  mcl.add(
589
654
  r"^\s*SUB_LOCAL\s+(?P<match>~?\w+)\s+(?P<repl>.+)$",
590
655
  x_sub_local,
591
- "SUB",
592
- "Define a local variable consisting of a string to match and a replacement for it.",
656
+ description="SUB_LOCAL",
657
+ category="action",
658
+ )
659
+ mcl.add(
660
+ r"^\s*SUB_ENCRYPT\s+(?P<match>[+~]?\w+)\s+(?P<plaintext>.+)\s*$",
661
+ x_sub_encrypt,
662
+ description="SUB_ENCRYPT",
663
+ category="action",
664
+ )
665
+ mcl.add(
666
+ r"^\s*SUB_DECRYPT\s+(?P<match>[+~]?\w+)\s+(?P<crypttext>.+)\s*$",
667
+ x_sub_decrypt,
668
+ description="SUB_DECRYPT",
669
+ category="action",
593
670
  )
594
- mcl.add(r"^\s*SUB_ENCRYPT\s+(?P<match>[+~]?\w+)\s+(?P<plaintext>.+)\s*$", x_sub_encrypt)
595
- mcl.add(r"^\s*SUB_DECRYPT\s+(?P<match>[+~]?\w+)\s+(?P<crypttext>.+)\s*$", x_sub_decrypt)
596
671
 
597
672
  # ------------------------------------------------------------------
598
673
  # WAIT_UNTIL
@@ -600,26 +675,71 @@ def build_dispatch_table() -> MetaCommandList:
600
675
  mcl.add(
601
676
  r"^\s*WAIT_UNTIL\s+(?P<condition>.+)\s+(?P<end>HALT|CONTINUE)\s+AFTER\s+(?P<seconds>\d+)\s+SECONDS\s*$",
602
677
  x_wait_until,
678
+ description="WAIT_UNTIL",
679
+ category="control",
603
680
  )
604
681
 
605
682
  # ------------------------------------------------------------------
606
683
  # CONFIG * (various settings)
607
684
  # ------------------------------------------------------------------
685
+ mcl.add(
686
+ r"^\s*LOG_WRITE_MESSAGES\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
687
+ x_logwritemessages,
688
+ description="LOG_WRITE_MESSAGES",
689
+ category="config_option",
690
+ )
608
691
  mcl.add(
609
692
  r"^\s*CONFIG\s+LOG_WRITE_MESSAGES\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
610
693
  x_logwritemessages,
694
+ description="CONFIG",
695
+ category="config",
611
696
  )
612
697
  mcl.add(
613
698
  r"^\s*CONFIG\s+QUOTE_ALL_TEXT\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
614
699
  x_quote_all_text,
700
+ description="QUOTE_ALL_TEXT",
701
+ category="config_option",
702
+ )
703
+ mcl.add(
704
+ r"^\s*CONFIG\s+IMPORT_ROW_BUFFER\s+(?P<rows>[1-9][0-9]*)\s*$",
705
+ x_import_row_buffer,
706
+ description="IMPORT_ROW_BUFFER",
707
+ category="config_option",
708
+ )
709
+ mcl.add(
710
+ r"^\s*CONFIG\s+SHOW_PROGRESS\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
711
+ x_show_progress,
712
+ description="SHOW_PROGRESS",
713
+ category="config_option",
714
+ )
715
+ mcl.add(
716
+ r"^\s*CONFIG\s+EXPORT_ROW_BUFFER\s+(?P<rows>[1-9][0-9]*)\s*$",
717
+ x_export_row_buffer,
718
+ description="EXPORT_ROW_BUFFER",
719
+ category="config_option",
720
+ )
721
+ mcl.add(
722
+ r"^\s*CONFIG\s+ZIP_BUFFER_MB\s+(?P<size>[1-9][0-9]*)\s*$",
723
+ x_zip_buffer_mb,
724
+ description="ZIP_BUFFER_MB",
725
+ category="config_option",
726
+ )
727
+ mcl.add(
728
+ r"^\s*EMPTY_STRINGS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
729
+ x_empty_strings,
730
+ description="EMPTY_STRINGS",
731
+ category="config_option",
615
732
  )
616
- mcl.add(r"^\s*CONFIG\s+IMPORT_ROW_BUFFER\s+(?P<rows>[1-9][0-9]*)\s*$", x_import_row_buffer)
617
- mcl.add(r"^\s*CONFIG\s+EXPORT_ROW_BUFFER\s+(?P<rows>[1-9][0-9]*)\s*$", x_export_row_buffer)
618
- mcl.add(r"^\s*CONFIG\s+ZIP_BUFFER_MB\s+(?P<size>[1-9][0-9]*)\s*$", x_zip_buffer_mb)
619
733
  mcl.add(
620
734
  r"^\s*CONFIG\s+EMPTY_STRINGS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
621
735
  x_empty_strings,
622
736
  )
737
+ mcl.add(
738
+ r"^\s*TRIM_STRINGS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
739
+ x_trim_strings,
740
+ description="TRIM_STRINGS",
741
+ category="config_option",
742
+ )
623
743
  mcl.add(
624
744
  r"^\s*CONFIG\s+TRIM_STRINGS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
625
745
  x_trim_strings,
@@ -627,18 +747,32 @@ def build_dispatch_table() -> MetaCommandList:
627
747
  mcl.add(
628
748
  r"^\s*CONFIG\s+REPLACE_NEWLINES\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
629
749
  x_replace_newlines,
750
+ description="REPLACE_NEWLINES",
751
+ category="config_option",
630
752
  )
631
753
  mcl.add(
632
754
  r"^\s*CONFIG\s+EMPTY_ROWS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
633
755
  x_empty_rows,
756
+ description="EMPTY_ROWS",
757
+ category="config_option",
634
758
  )
635
759
  mcl.add(
636
760
  r"^\s*CONFIG\s+ONLY_STRINGS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
637
761
  x_only_strings,
762
+ description="ONLY_STRINGS",
763
+ category="config_option",
638
764
  )
639
765
  mcl.add(
640
766
  r"^\s*(?:CONFIG\s+)?BOOLEAN_INT\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
641
767
  x_boolean_int,
768
+ description="BOOLEAN_INT",
769
+ category="config_option",
770
+ )
771
+ mcl.add(
772
+ r"^\s*BOOLEAN_WORDS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
773
+ x_boolean_words,
774
+ description="BOOLEAN_WORDS",
775
+ category="config_option",
642
776
  )
643
777
  mcl.add(
644
778
  r"^\s*CONFIG\s+BOOLEAN_WORDS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
@@ -647,10 +781,20 @@ def build_dispatch_table() -> MetaCommandList:
647
781
  mcl.add(
648
782
  r"^\s*CONFIG\s+FOLD_COLUMN_HEADERS\s+(?P<foldspec>no|lower|upper)\s*$",
649
783
  x_fold_col_hdrs,
784
+ description="FOLD_COLUMN_HEADERS",
785
+ category="config_option",
650
786
  )
651
787
  mcl.add(
652
788
  r"^\s*CONFIG\s+TRIM_COLUMN_HEADERS\s+(?P<which>NONE|BOTH|LEFT|RIGHT)\s*$",
653
789
  x_trim_col_hdrs,
790
+ description="TRIM_COLUMN_HEADERS",
791
+ category="config_option",
792
+ )
793
+ mcl.add(
794
+ r"^\s*CLEAN_COLUMN_HEADERS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
795
+ x_clean_col_hdrs,
796
+ description="CLEAN_COLUMN_HEADERS",
797
+ category="config_option",
654
798
  )
655
799
  mcl.add(
656
800
  r"^\s*CONFIG\s+CLEAN_COLUMN_HEADERS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
@@ -659,44 +803,119 @@ def build_dispatch_table() -> MetaCommandList:
659
803
  mcl.add(
660
804
  r"^\s*CONFIG\s+DELETE_EMPTY_COLUMNS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
661
805
  x_del_empty_cols,
806
+ description="DELETE_EMPTY_COLUMNS",
807
+ category="config_option",
662
808
  )
663
809
  mcl.add(
664
810
  r"^\s*CONFIG\s+CREATE_COLUMN_HEADERS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
665
811
  x_create_col_hdrs,
812
+ description="CREATE_COLUMN_HEADERS",
813
+ category="config_option",
666
814
  )
667
815
  mcl.add(
668
816
  r"^\s*CONFIG\s+DEDUP_COLUMN_HEADERS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
669
817
  x_dedup_col_hdrs,
818
+ description="DEDUP_COLUMN_HEADERS",
819
+ category="config_option",
820
+ )
821
+ mcl.add(
822
+ r"^\s*IMPORT_ONLY_COMMON_COLUMNS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
823
+ x_import_common_cols_only,
824
+ description="IMPORT_ONLY_COMMON_COLUMNS",
825
+ category="config_option",
826
+ )
827
+ mcl.add(
828
+ r"^\s*IMPORT_COMMON_COLUMNS_ONLY\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
829
+ x_import_common_cols_only,
670
830
  )
671
831
  mcl.add(
672
832
  r"^\s*CONFIG\s+IMPORT_ONLY_COMMON_COLUMNS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
673
833
  x_import_common_cols_only,
674
834
  )
835
+ mcl.add(
836
+ r"^\s*MAKE_EXPORT_DIRS\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
837
+ x_make_export_dirs,
838
+ description="MAKE_EXPORT_DIRS",
839
+ category="config_option",
840
+ )
675
841
  mcl.add(
676
842
  r"^\s*CONFIG\s+MAKE_EXPORT_DIRS\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
677
843
  x_make_export_dirs,
678
844
  )
845
+ mcl.add(
846
+ r"^\s*WRITE_WARNINGS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
847
+ x_write_warnings,
848
+ description="WRITE_WARNINGS",
849
+ category="config_option",
850
+ )
679
851
  mcl.add(
680
852
  r"^\s*CONFIG\s+WRITE_WARNINGS\s+(?P<yesno>YES|NO|ON|OFF|TRUE|FALSE|0|1)\s*$",
681
853
  x_write_warnings,
682
854
  )
683
- mcl.add(r"^\s*CONFIG\s+GUI_LEVEL\s+(?P<level>[0-2])\s*$", x_gui_level)
684
- mcl.add(r"^\s*CONFIG\s+WRITE_PREFIX\s+(?P<prefix>.*)\s*$", x_write_prefix)
685
- mcl.add(r"^\s*CONFIG\s+WRITE_SUFFIX\s+(?P<suffix>.*)\s*$", x_write_suffix)
686
- mcl.add(r"^\s*CONFIG\s+SCAN_LINES\s+(?P<scanlines>[0-9]+)\s*$", x_scan_lines)
687
- mcl.add(r"^\s*CONFIG\s+HDF5_TEXT_LEN\s+(?P<textlen>[0-9]+)\s*$", x_hdf5_text_len)
855
+ mcl.add(
856
+ r"^\s*CONFIG\s+GUI_LEVEL\s+(?P<level>[0-2])\s*$",
857
+ x_gui_level,
858
+ description="GUI_LEVEL",
859
+ category="config_option",
860
+ )
861
+ mcl.add(
862
+ r"^\s*CONFIG\s+WRITE_PREFIX\s+(?P<prefix>.*)\s*$",
863
+ x_write_prefix,
864
+ description="WRITE_PREFIX",
865
+ category="config_option",
866
+ )
867
+ mcl.add(
868
+ r"^\s*CONFIG\s+WRITE_SUFFIX\s+(?P<suffix>.*)\s*$",
869
+ x_write_suffix,
870
+ description="WRITE_SUFFIX",
871
+ category="config_option",
872
+ )
873
+ mcl.add(
874
+ r"^\s*CONFIG\s+SCAN_LINES\s+(?P<scanlines>[0-9]+)\s*$",
875
+ x_scan_lines,
876
+ description="SCAN_LINES",
877
+ category="config_option",
878
+ )
879
+ mcl.add(
880
+ r"^\s*CONFIG\s+HDF5_TEXT_LEN\s+(?P<textlen>[0-9]+)\s*$",
881
+ x_hdf5_text_len,
882
+ description="HDF5_TEXT_LEN",
883
+ category="config_option",
884
+ )
688
885
  mcl.add(
689
886
  r"^\s*CONFIG\s+LOG_DATAVARS\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
690
887
  x_log_datavars,
888
+ description="LOG_DATAVARS",
889
+ category="config_option",
890
+ )
891
+ mcl.add(
892
+ r"^\s*CONFIG\s+LOG_SQL\s+(?P<setting>Yes|No|On|Off|True|False|0|1)\s*$",
893
+ x_log_sql,
894
+ description="LOG_SQL",
895
+ category="config_option",
691
896
  )
692
897
  mcl.add(
693
898
  r"^\s*CONFIG\s+DAO_FLUSH_DELAY_SECS\s+(?P<secs>[0-9]*\.?[0-9]+)\s*$",
694
899
  x_daoflushdelay,
900
+ description="DAO_FLUSH_DELAY_SECS",
901
+ category="config_option",
902
+ )
903
+ mcl.add(
904
+ r"^\s*CONSOLE\s+WAIT_WHEN_ERROR\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE|0|1)\s*$",
905
+ x_consolewait_onerror,
906
+ description="CONSOLE_WAIT_WHEN_ERROR",
907
+ category="config_option",
695
908
  )
696
909
  mcl.add(
697
910
  r"^\s*CONFIG\s+CONSOLE\s+WAIT_WHEN_ERROR\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE|0|1)\s*$",
698
911
  x_consolewait_onerror,
699
912
  )
913
+ mcl.add(
914
+ r"^\s*CONSOLE\s+WAIT_WHEN_DONE\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE|0|1)\s*$",
915
+ x_consolewait_whendone,
916
+ description="CONSOLE_WAIT_WHEN_DONE",
917
+ category="config_option",
918
+ )
700
919
  mcl.add(
701
920
  r"^\s*CONFIG\s+CONSOLE\s+WAIT_WHEN_DONE\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE|0|1)\s*$",
702
921
  x_consolewait_whendone,
@@ -713,6 +932,8 @@ def build_dispatch_table() -> MetaCommandList:
713
932
  r"(?:\s*,\s*ENCODING\s*=\s*(?P<encoding>[A-Z][A-Z0-9_-]+))?\s*\)\s+AS\s+(?P<db_alias>[A-Z][A-Z0-9_]*)\s*$",
714
933
  ),
715
934
  x_connect_access,
935
+ description="CONNECT",
936
+ category="action",
716
937
  )
717
938
 
718
939
  # ------------------------------------------------------------------
@@ -773,22 +994,45 @@ def build_dispatch_table() -> MetaCommandList:
773
994
  mcl.add(
774
995
  r'^\s*EXEC(?:UTE)?\s+SCRIPT(?:\s+(?P<exists>IF\s+EXISTS))?\s+(?P<script_id>\w+)(?:(?:\s+WITH)?(?:\s+ARG(?:UMENT)?S?)?\s*\(\s*(?P<argexp>#?\w+\s*=\s*(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\]))(?:\s*,\s*#?\w+\s*=\s*(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\])))*)\s*\))?(?:\s+(?P<looptype>WHILE|UNTIL)\s*\(\s*(?P<loopcond>.+)\s*\))?\s*$',
775
996
  x_executescript,
997
+ description="EXECUTE SCRIPT",
998
+ category="action",
776
999
  )
777
1000
  mcl.add(
778
1001
  r'^\s*RUN\s+SCRIPT(?:\s+(?P<exists>IF\s+EXISTS))?\s+(?P<script_id>\w+)(?:(?:\s+WITH)?(?:\s+ARG(?:UMENT)?S?)?\s*\(\s*(?P<argexp>#?\w+\s*=\s*(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\]))(?:\s*,\s*#?\w+\s*=\s*(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\])))*)\s*\))?(?:\s+(?P<looptype>WHILE|UNTIL)\s*\(\s*(?P<loopcond>.+)\s*\))?\s*$',
779
1002
  x_executescript,
1003
+ description="RUN",
1004
+ category="action",
780
1005
  )
781
1006
  mcl.add(
782
1007
  r"^\s*(?P<cmd>RUN|EXECUTE)\s+(?P<queryname>\#?\w+)\s*$",
783
1008
  x_execute,
784
- "RUN|EXECUTE",
785
- "Run a database function, view, or action query (DBMS-dependent)",
1009
+ description="RUN",
1010
+ category="action",
786
1011
  )
787
1012
 
788
1013
  # ------------------------------------------------------------------
789
1014
  # ON ERROR_HALT / ON CANCEL_HALT
790
1015
  # ------------------------------------------------------------------
791
- mcl.add(r"^\s*ON\s+ERROR_HALT\s+WRITE\s+CLEAR\s*$", x_error_halt_write_clear)
1016
+ mcl.add(
1017
+ r"^\s*ON\s+ERROR_HALT\s+WRITE\s+CLEAR\s*$",
1018
+ x_error_halt_write_clear,
1019
+ description="ON ERROR_HALT",
1020
+ category="config",
1021
+ )
1022
+ mcl.add(
1023
+ ins_fn_rxs(
1024
+ r"^\s*ON\s+ERROR_HALT\s+WRITE\s+\'(?P<text>([^\']|\n)*)\'(?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
1025
+ r")?\s*$",
1026
+ ),
1027
+ x_error_halt_write,
1028
+ )
1029
+ mcl.add(
1030
+ ins_fn_rxs(
1031
+ r"^\s*ON\s+ERROR_HALT\s+WRITE\s+\[(?P<text>([^\]]|\n)*)\](?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
1032
+ r")?\s*$",
1033
+ ),
1034
+ x_error_halt_write,
1035
+ )
792
1036
  mcl.add(
793
1037
  ins_fn_rxs(
794
1038
  r'^\s*ON\s+ERROR_HALT\s+WRITE\s+"(?P<text>([^"]|\n)*)"(?:(?:\s+(?P<tee>TEE))?\s+TO\s+',
@@ -816,7 +1060,26 @@ def build_dispatch_table() -> MetaCommandList:
816
1060
  x_error_halt_email,
817
1061
  )
818
1062
  mcl.add(r"^\s*ON\s+ERROR_HALT\s+EXEC\s+CLEAR\s*$", x_error_halt_exec_clear)
819
- mcl.add(r"^\s*ON\s+CANCEL_HALT\s+WRITE\s+CLEAR\s*$", x_cancel_halt_write_clear)
1063
+ mcl.add(
1064
+ r"^\s*ON\s+CANCEL_HALT\s+WRITE\s+CLEAR\s*$",
1065
+ x_cancel_halt_write_clear,
1066
+ description="ON CANCEL_HALT",
1067
+ category="config",
1068
+ )
1069
+ mcl.add(
1070
+ ins_fn_rxs(
1071
+ r"^\s*ON\s+CANCEL_HALT\s+WRITE\s+\'(?P<text>([^\']|\n)*)\'(?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
1072
+ r")?\s*$",
1073
+ ),
1074
+ x_cancel_halt_write,
1075
+ )
1076
+ mcl.add(
1077
+ ins_fn_rxs(
1078
+ r"^\s*ON\s+CANCEL_HALT\s+WRITE\s+\[(?P<text>([^\]]|\n)*)\](?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
1079
+ r")?\s*$",
1080
+ ),
1081
+ x_cancel_halt_write,
1082
+ )
820
1083
  mcl.add(
821
1084
  ins_fn_rxs(
822
1085
  r'^\s*ON\s+CANCEL_HALT\s+WRITE\s+"(?P<text>([^"]|\n)*)"(?:(?:\s+(?P<tee>TEE))?\s+TO\s+',
@@ -848,7 +1111,12 @@ def build_dispatch_table() -> MetaCommandList:
848
1111
  # ------------------------------------------------------------------
849
1112
  # SUB_TEMPFILE
850
1113
  # ------------------------------------------------------------------
851
- mcl.add(r"^\s*SUB_TEMPFILE\s+(?P<match>[+~]?\w+)\s*$", x_sub_tempfile)
1114
+ mcl.add(
1115
+ r"^\s*SUB_TEMPFILE\s+(?P<match>[+~]?\w+)\s*$",
1116
+ x_sub_tempfile,
1117
+ description="SUB_TEMPFILE",
1118
+ category="action",
1119
+ )
852
1120
 
853
1121
  # ------------------------------------------------------------------
854
1122
  # WRITE CREATE_TABLE (ODS / XLS / CSV / alias)
@@ -874,6 +1142,8 @@ def build_dispatch_table() -> MetaCommandList:
874
1142
  ),
875
1143
  ),
876
1144
  x_write_create_table_ods,
1145
+ description="WRITE CREATE_TABLE",
1146
+ category="action",
877
1147
  )
878
1148
  mcl.add(
879
1149
  ins_table_rxs(
@@ -931,11 +1201,18 @@ def build_dispatch_table() -> MetaCommandList:
931
1201
  # ------------------------------------------------------------------
932
1202
  # RESET / SET COUNTER
933
1203
  # ------------------------------------------------------------------
934
- mcl.add(r"^\s*RESET\s+COUNTER\s+(?P<counter_no>\d+)\s*$", x_reset_counter)
1204
+ mcl.add(
1205
+ r"^\s*RESET\s+COUNTER\s+(?P<counter_no>\d+)\s*$",
1206
+ x_reset_counter,
1207
+ description="RESET COUNTER",
1208
+ category="action",
1209
+ )
935
1210
  mcl.add(r"^\s*RESET\s+COUNTERS\s*$", x_reset_counters)
936
1211
  mcl.add(
937
1212
  r"^\s*SET\s+COUNTER\s+(?P<counter_no>\d+)\s+TO\s+(?P<value>[0-9+\-*/() ]+)\s*$",
938
1213
  x_set_counter,
1214
+ description="SET COUNTER",
1215
+ category="action",
939
1216
  )
940
1217
 
941
1218
  # ------------------------------------------------------------------
@@ -947,6 +1224,8 @@ def build_dispatch_table() -> MetaCommandList:
947
1224
  r'^\s*PROMPT(?:\s+"(?P<message>(.|\n)*)")?\s+CREDENTIALS\s+(?P<user>\w+)\s+(?P<pw>\w+)\s*$',
948
1225
  ),
949
1226
  x_prompt_credentials,
1227
+ description="PROMPT CREDENTIALS",
1228
+ category="prompt",
950
1229
  )
951
1230
  mcl.add(
952
1231
  (
@@ -960,22 +1239,31 @@ def build_dispatch_table() -> MetaCommandList:
960
1239
  r'^\s*CONNECT\s+PROMPT(?:\s+"(?P<message>(.|\n)*)")?\s+AS\s+(?P<alias>\w+)(?:\s+HELP\s+"(?P<help>[^"]+)")?\s*$',
961
1240
  ),
962
1241
  x_prompt_connect,
1242
+ description="PROMPT CONNECT",
1243
+ category="prompt",
963
1244
  )
964
1245
 
965
1246
  # ------------------------------------------------------------------
966
1247
  # TIMER / LOG / SUB_INI
967
1248
  # ------------------------------------------------------------------
968
- mcl.add(r"^\s*TIMER\s+(?P<onoff>ON|OFF)\s*$", x_timer)
969
- mcl.add(r'^\s*LOG\s+"(?P<message>.+)"\s*$', x_log)
1249
+ mcl.add(r"^\s*TIMER\s+(?P<onoff>ON|OFF)\s*$", x_timer, description="TIMER", category="config")
1250
+ mcl.add(r'^\s*LOG\s+"(?P<message>.+)"\s*$', x_log, description="LOG", category="action")
970
1251
  mcl.add(
971
1252
  ins_fn_rxs(r"^\s*SUB_INI\s+(?:FILE\s+)?", r"(?:\s+SECTION)?\s+(?P<section>\w+)\s*$"),
972
1253
  x_sub_ini,
1254
+ description="SUB_INI",
1255
+ category="action",
973
1256
  )
974
1257
 
975
1258
  # ------------------------------------------------------------------
976
1259
  # CONSOLE
977
1260
  # ------------------------------------------------------------------
978
- mcl.add(r"^\s*CONSOLE\s+(?P<hideshow>HIDE|SHOW)\s*$", x_console_hideshow)
1261
+ mcl.add(
1262
+ r"^\s*CONSOLE\s+(?P<hideshow>HIDE|SHOW)\s*$",
1263
+ x_console_hideshow,
1264
+ description="CONSOLE",
1265
+ category="prompt",
1266
+ )
979
1267
  mcl.add(r"^\s*CONSOLE\s+WIDTH\s+(?P<width>\d+)\s*$", x_consolewidth)
980
1268
  mcl.add(r"^\s*CONSOLE\s+HEIGHT\s+(?P<height>\d+)\s*$", x_consoleheight)
981
1269
  mcl.add(r'^\s*CONSOLE\s+STATUS\s+"(?P<message>.*)"\s*$', x_consolestatus)
@@ -993,8 +1281,18 @@ def build_dispatch_table() -> MetaCommandList:
993
1281
  # ------------------------------------------------------------------
994
1282
  # DISCONNECT / AUTOCOMMIT
995
1283
  # ------------------------------------------------------------------
996
- mcl.add(r"^\s*DISCONNECT(?:(?:\s+FROM)?\s+(?P<alias>[A-Z][A-Z0-9_]*))?\s*$", x_disconnect)
997
- mcl.add(r"^\s*AUTOCOMMIT\s+OFF\s*$", x_autocommit_off)
1284
+ mcl.add(
1285
+ r"^\s*DISCONNECT(?:(?:\s+FROM)?\s+(?P<alias>[A-Z][A-Z0-9_]*))?\s*$",
1286
+ x_disconnect,
1287
+ description="DISCONNECT",
1288
+ category="action",
1289
+ )
1290
+ mcl.add(
1291
+ r"^\s*AUTOCOMMIT\s+OFF\s*$",
1292
+ x_autocommit_off,
1293
+ description="AUTOCOMMIT",
1294
+ category="action",
1295
+ )
998
1296
  mcl.add(r"^\s*AUTOCOMMIT\s+ON(?:\s+WITH\s+(?P<action>COMMIT|ROLLBACK))?\s*$", x_autocommit_on)
999
1297
 
1000
1298
  # ------------------------------------------------------------------
@@ -1006,9 +1304,11 @@ def build_dispatch_table() -> MetaCommandList:
1006
1304
  r")?\s*$",
1007
1305
  ),
1008
1306
  x_writescript,
1307
+ description="WRITE SCRIPT",
1308
+ category="action",
1009
1309
  )
1010
- mcl.add(r"^\s*MAX_INT\s+(?P<maxint>[0-9]+)\s*$", x_max_int)
1011
- mcl.add(r"^\s*PG_VACUUM(?P<vacuum_args>.*)\s*$", x_pg_vacuum)
1310
+ mcl.add(r"^\s*MAX_INT\s+(?P<maxint>[0-9]+)\s*$", x_max_int, description="MAX_INT", category="action")
1311
+ mcl.add(r"^\s*PG_VACUUM(?P<vacuum_args>.*)\s*$", x_pg_vacuum, description="PG_VACUUM", category="action")
1012
1312
 
1013
1313
  # ------------------------------------------------------------------
1014
1314
  # ZIP
@@ -1020,6 +1320,8 @@ def build_dispatch_table() -> MetaCommandList:
1020
1320
  symbolicname="zipfilename",
1021
1321
  ),
1022
1322
  x_zip,
1323
+ description="ZIP",
1324
+ category="action",
1023
1325
  )
1024
1326
  mcl.add(
1025
1327
  ins_fn_rxs(
@@ -1039,6 +1341,8 @@ def build_dispatch_table() -> MetaCommandList:
1039
1341
  r"))?)?(?:\s+EXIT_STATUS\s+(?P<errorlevel>\d+))?\s*$",
1040
1342
  ),
1041
1343
  x_halt,
1344
+ description="HALT",
1345
+ category="control",
1042
1346
  )
1043
1347
  for errmsg_delim in (r"\[", r"\#", r"\`", r"\'", r"\~", r'"'):
1044
1348
  # Use the same open/close bracket pair for the errmsg capture
@@ -1059,25 +1363,42 @@ def build_dispatch_table() -> MetaCommandList:
1059
1363
  # ------------------------------------------------------------------
1060
1364
  # BEGIN / END BATCH / ROLLBACK
1061
1365
  # ------------------------------------------------------------------
1062
- mcl.add(r"^\s*BEGIN\s+BATCH\s*$", x_begin_batch)
1063
- mcl.add(r"^\s*END\s+BATCH\s*$", x_end_batch, "END BATCH", run_in_batch=True)
1064
- mcl.add(r"^\s*ROLLBACK(:?\s+BATCH)?\s*$", x_rollback, "ROLLBACK BATCH", run_in_batch=True)
1366
+ mcl.add(r"^\s*BEGIN\s+BATCH\s*$", x_begin_batch, description="BEGIN BATCH", category="block")
1367
+ mcl.add(r"^\s*END\s+BATCH\s*$", x_end_batch, "END BATCH", run_in_batch=True, category="block")
1368
+ mcl.add(r"^\s*ROLLBACK(:?\s+BATCH)?\s*$", x_rollback, "ROLLBACK BATCH", run_in_batch=True, category="block")
1065
1369
 
1066
1370
  # ------------------------------------------------------------------
1067
1371
  # ERROR_HALT / METACOMMAND_ERROR_HALT / CANCEL_HALT
1068
1372
  # ------------------------------------------------------------------
1069
- mcl.add(r"\s*ERROR_HALT\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE)\s*$", x_error_halt)
1373
+ mcl.add(
1374
+ r"\s*ERROR_HALT\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE)\s*$",
1375
+ x_error_halt,
1376
+ description="ERROR_HALT",
1377
+ category="control",
1378
+ )
1070
1379
  mcl.add(
1071
1380
  r"\s*METACOMMAND_ERROR_HALT\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE)\s*$",
1072
1381
  x_metacommand_error_halt,
1073
1382
  set_error_flag=False,
1383
+ description="METACOMMAND_ERROR_HALT",
1384
+ category="control",
1385
+ )
1386
+ mcl.add(
1387
+ r"^\s*CANCEL_HALT\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE)\s*$",
1388
+ x_cancel_halt,
1389
+ description="CANCEL_HALT",
1390
+ category="control",
1074
1391
  )
1075
- mcl.add(r"^\s*CANCEL_HALT\s+(?P<onoff>ON|OFF|YES|NO|TRUE|FALSE)\s*$", x_cancel_halt)
1076
1392
 
1077
1393
  # ------------------------------------------------------------------
1078
1394
  # LOOP
1079
1395
  # ------------------------------------------------------------------
1080
- mcl.add(r"^\s*LOOP\s+(?P<looptype>WHILE|UNTIL)\s*\(\s*(?P<loopcond>.+)\s*\)\s*$", x_loop)
1396
+ mcl.add(
1397
+ r"^\s*LOOP\s+(?P<looptype>WHILE|UNTIL)\s*\(\s*(?P<loopcond>.+)\s*\)\s*$",
1398
+ x_loop,
1399
+ description="LOOP",
1400
+ category="control",
1401
+ )
1081
1402
 
1082
1403
  # ------------------------------------------------------------------
1083
1404
  # PAUSE
@@ -1089,6 +1410,8 @@ def build_dispatch_table() -> MetaCommandList:
1089
1410
  r"^\s*PAUSE\s+\[(?P<text>.+)\](?:\s+(?P<action>HALT|CONTINUE)\s+AFTER\s+(?P<countdown>\d+(?:\.\d*)?)\s+(?P<timeunit>SECONDS|MINUTES))?\s*$",
1090
1411
  ),
1091
1412
  x_pause,
1413
+ description="PAUSE",
1414
+ category="control",
1092
1415
  )
1093
1416
 
1094
1417
  # ------------------------------------------------------------------
@@ -1101,6 +1424,8 @@ def build_dispatch_table() -> MetaCommandList:
1101
1424
  r'(?:\s+INITIALLY\s+"(?P<initial>[^"]+)")?(?:\s+HELP\s+(?P<help>[^\s]+))?\s*$',
1102
1425
  ),
1103
1426
  x_prompt_enter,
1427
+ description="PROMPT ENTER_SUB",
1428
+ category="prompt",
1104
1429
  )
1105
1430
  mcl.add(
1106
1431
  ins_table_rxs(
@@ -1124,6 +1449,8 @@ def build_dispatch_table() -> MetaCommandList:
1124
1449
  ),
1125
1450
  ),
1126
1451
  x_prompt_entryform,
1452
+ description="PROMPT ENTRY_FORM",
1453
+ category="prompt",
1127
1454
  )
1128
1455
  mcl.add(
1129
1456
  ins_table_rxs(
@@ -1146,6 +1473,8 @@ def build_dispatch_table() -> MetaCommandList:
1146
1473
  r'\s+MESSAGE\s+"(?P<msg>(.|\n)*)"(?:\s+(?P<cont>CONTINUE))?(?:\s+HELP\s(?P<help>[^\s]+))?\s*$',
1147
1474
  ),
1148
1475
  x_prompt_selectsub,
1476
+ description="PROMPT SELECT_SUB",
1477
+ category="prompt",
1149
1478
  )
1150
1479
  mcl.add(
1151
1480
  ins_table_rxs(
@@ -1165,6 +1494,8 @@ def build_dispatch_table() -> MetaCommandList:
1165
1494
  r"^\s*PROMPT\s+PAUSE\s+\[(?P<text>.+)\](?:\s+(?P<action>HALT|CONTINUE)\s+AFTER\s+(?P<countdown>\d+(?:\.\d*)?)\s+(?P<timeunit>SECONDS|MINUTES))?\s*$",
1166
1495
  ),
1167
1496
  x_prompt_pause,
1497
+ description="PROMPT PAUSE",
1498
+ category="prompt",
1168
1499
  )
1169
1500
 
1170
1501
  # ------------------------------------------------------------------
@@ -1177,6 +1508,8 @@ def build_dispatch_table() -> MetaCommandList:
1177
1508
  r"^\s*ASK\s+\[(?P<question>.+)\]\s+SUB\s+(?P<match>~?\w+)\s*$",
1178
1509
  ),
1179
1510
  x_ask,
1511
+ description="ASK",
1512
+ category="prompt",
1180
1513
  )
1181
1514
 
1182
1515
  # ------------------------------------------------------------------
@@ -1194,6 +1527,8 @@ def build_dispatch_table() -> MetaCommandList:
1194
1527
  suffix="1",
1195
1528
  ),
1196
1529
  x_prompt_compare,
1530
+ description="PROMPT COMPARE",
1531
+ category="prompt",
1197
1532
  )
1198
1533
  mcl.add(
1199
1534
  ins_table_rxs(
@@ -1220,6 +1555,8 @@ def build_dispatch_table() -> MetaCommandList:
1220
1555
  suffix="1",
1221
1556
  ),
1222
1557
  x_prompt_ask_compare,
1558
+ description="PROMPT ASK COMPARE",
1559
+ category="prompt",
1223
1560
  )
1224
1561
  mcl.add(
1225
1562
  ins_table_rxs(
@@ -1246,6 +1583,8 @@ def build_dispatch_table() -> MetaCommandList:
1246
1583
  r"(?:\s+SYMBOL\s+(?P<symbol_col>\w+))?\s*$",
1247
1584
  ),
1248
1585
  x_prompt_map,
1586
+ description="PROMPT MAP",
1587
+ category="prompt",
1249
1588
  )
1250
1589
  mcl.add(
1251
1590
  ins_table_rxs(
@@ -1277,6 +1616,8 @@ def build_dispatch_table() -> MetaCommandList:
1277
1616
  "att_file",
1278
1617
  ),
1279
1618
  x_email,
1619
+ description="EMAIL",
1620
+ category="action",
1280
1621
  )
1281
1622
 
1282
1623
  # ------------------------------------------------------------------
@@ -1288,10 +1629,12 @@ def build_dispatch_table() -> MetaCommandList:
1288
1629
  r"^\s*EXPORT_METADATA(?:\s+(?P<append>APPEND))?(?:\s+(?P<all>ALL))?\s+TO\s+",
1289
1630
  r"(?:\s+IN\s+ZIPFILE\s+",
1290
1631
  ),
1291
- r")?\s+AS\s+(?P<format>CSV|TAB|TSV|TABQ|TSVQ|TXT|TEXT)",
1632
+ rf")?\s+AS\s+(?P<format>{'|'.join(METADATA_FORMATS)})",
1292
1633
  symbolicname="zipfilename",
1293
1634
  ),
1294
1635
  x_export_metadata,
1636
+ description="EXPORT_METADATA",
1637
+ category="action",
1295
1638
  )
1296
1639
  mcl.add(
1297
1640
  ins_table_rxs(
@@ -1304,23 +1647,52 @@ def build_dispatch_table() -> MetaCommandList:
1304
1647
  # ------------------------------------------------------------------
1305
1648
  # SUB operations
1306
1649
  # ------------------------------------------------------------------
1307
- mcl.add(r"^\s*SUB_EMPTY\s+(?P<match>[+~]?\w+)\s*$", x_sub_empty)
1650
+ mcl.add(
1651
+ r"^\s*SUB_EMPTY\s+(?P<match>[+~]?\w+)\s*$",
1652
+ x_sub_empty,
1653
+ description="SUB_EMPTY",
1654
+ category="action",
1655
+ )
1308
1656
  mcl.add(
1309
1657
  r"^\s*SUB_ADD\s+(?P<match>[+~]?\w+)\s+(?P<increment>[+\-0-9\.*/() ]+)\s*$",
1310
1658
  x_sub_add,
1659
+ description="SUB_ADD",
1660
+ category="action",
1661
+ )
1662
+ mcl.add(
1663
+ r"^\s*SUB_APPEND\s+(?P<match>[+~]?\w+)\s(?P<repl>(.|\n)*)$",
1664
+ x_sub_append,
1665
+ description="SUB_APPEND",
1666
+ category="action",
1311
1667
  )
1312
- mcl.add(r"^\s*SUB_APPEND\s+(?P<match>[+~]?\w+)\s(?P<repl>(.|\n)*)$", x_sub_append)
1313
1668
 
1314
1669
  # ------------------------------------------------------------------
1315
1670
  # IF / ORIF / ANDIF / ELSEIF / ELSE / ENDIF
1316
1671
  # ------------------------------------------------------------------
1317
- mcl.add(r"^\s*ORIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_orif, run_when_false=True)
1318
- mcl.add(r"^\s*ELSEIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_elseif, run_when_false=True)
1319
- mcl.add(r"^\s*ANDIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_andif)
1320
- mcl.add(r"^\s*ELSE\s*$", x_if_else, run_when_false=True)
1321
- mcl.add(r"^\s*IF\s*\(\s*(?P<condtest>.+)\s*\)\s*{\s*(?P<condcmd>.+)\s*}\s*$", x_if)
1322
- mcl.add(r"^\s*IF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_block, run_when_false=True)
1323
- mcl.add(r"^\s*ENDIF\s*$", x_if_end, run_when_false=True)
1672
+ mcl.add(
1673
+ r"^\s*ORIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$",
1674
+ x_if_orif,
1675
+ description="ORIF",
1676
+ run_when_false=True,
1677
+ category="control",
1678
+ )
1679
+ mcl.add(
1680
+ r"^\s*ELSEIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$",
1681
+ x_if_elseif,
1682
+ description="ELSEIF",
1683
+ run_when_false=True,
1684
+ category="control",
1685
+ )
1686
+ mcl.add(r"^\s*ANDIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_andif, description="ANDIF", category="control")
1687
+ mcl.add(r"^\s*ELSE\s*$", x_if_else, description="ELSE", run_when_false=True, category="control")
1688
+ mcl.add(
1689
+ r"^\s*IF\s*\(\s*(?P<condtest>.+)\s*\)\s*{\s*(?P<condcmd>.+)\s*}\s*$",
1690
+ x_if,
1691
+ description="IF",
1692
+ category="control",
1693
+ )
1694
+ mcl.add(r"^\s*IF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_block, run_when_false=True, category="control")
1695
+ mcl.add(r"^\s*ENDIF\s*$", x_if_end, description="ENDIF", run_when_false=True, category="control")
1324
1696
 
1325
1697
  # ------------------------------------------------------------------
1326
1698
  # CONNECT — SQL Server
@@ -1361,6 +1733,8 @@ def build_dispatch_table() -> MetaCommandList:
1361
1733
  r" IN\s+(?P<alias2>[A-Z][A-Z0-9_]*)\s*$",
1362
1734
  ),
1363
1735
  x_copy_query,
1736
+ description="COPY QUERY",
1737
+ category="action",
1364
1738
  )
1365
1739
  mcl.add(
1366
1740
  (
@@ -1371,6 +1745,8 @@ def build_dispatch_table() -> MetaCommandList:
1371
1745
  r"^COPY\s+(?:\[(?P<schema1>[A-Z][A-Z0-9_\-\/\: ]*)\]\.)?\[(?P<table1>[A-Z][A-Z0-9_\-\/\: ]*)\]\s+FROM\s+(?P<alias1>[A-Z][A-Z0-9_]*)\s+TO\s+(?:(?P<new>NEW|REPLACEMENT)\s+)?(?:\[(?P<schema2>[A-Z][A-Z0-9_\-\/\:]*)\]\.)?\[(?P<table2>[A-Z][A-Z0-9_\-\/\:]*)\]\s+IN\s+(?P<alias2>[A-Z][A-Z0-9_]*)\s*$",
1372
1746
  ),
1373
1747
  x_copy,
1748
+ description="COPY",
1749
+ category="action",
1374
1750
  )
1375
1751
 
1376
1752
  # ------------------------------------------------------------------
@@ -1379,10 +1755,14 @@ def build_dispatch_table() -> MetaCommandList:
1379
1755
  mcl.add(
1380
1756
  r"\s*APPEND\s+SCRIPT\s+(?P<script1>\w+)\s+TO\s+(?P<script2>\w+)\s*$",
1381
1757
  x_extendscript,
1758
+ description="APPEND SCRIPT",
1759
+ category="action",
1382
1760
  )
1383
1761
  mcl.add(
1384
1762
  r"\s*EXTEND\s+SCRIPT\s+(?P<script>\w+)\s+WITH\s+METACOMMAND\s+(?P<cmd>.+)\s*$",
1385
1763
  x_extendscript_metacommand,
1764
+ description="EXTEND SCRIPT",
1765
+ category="action",
1386
1766
  )
1387
1767
  mcl.add(
1388
1768
  r"\s*EXTEND\s+SCRIPT\s+(?P<script>\w+)\s+WITH\s+SQL\s+(?P<sql>.+;)\s*$",
@@ -1466,9 +1846,20 @@ def build_dispatch_table() -> MetaCommandList:
1466
1846
  )
1467
1847
 
1468
1848
  # ------------------------------------------------------------------
1469
- # USE / CONNECT DSN
1849
+ # CONNECT DSN
1470
1850
  # ------------------------------------------------------------------
1471
- mcl.add(r"^USE\s+(?P<db_alias>[A-Z][A-Z0-9_]*)\s*$", x_use)
1851
+ mcl.add(
1852
+ r"^CONNECT\s+TO\s+DSN\s*\(\s*DSN\s*=\s*(?P<dsn>[A-Z0-9][A-Z0-9_\-\.]*)\s*"
1853
+ r"(?:\s*,\s*USER\s*=\s*(?P<user>[A-Z][A-Z0-9_@\-\.]*)\s*,\s*NEED_PWD\s*=\s*(?P<need_pwd>TRUE|FALSE))?"
1854
+ r"(?:\s*,\s+PASSWORD\s*=\s*(?P<password>[^\s\)]+))?"
1855
+ r"(?:\s*,\s*ENCODING\s*=\s*(?P<encoding>[A-Z][A-Z0-9_-]+))?\s*\)\s+AS\s+(?P<db_alias>[A-Z][A-Z0-9_]*)\s*$",
1856
+ x_connect_dsn,
1857
+ )
1858
+
1859
+ # ------------------------------------------------------------------
1860
+ # USE
1861
+ # ------------------------------------------------------------------
1862
+ mcl.add(r"^USE\s+(?P<db_alias>[A-Z][A-Z0-9_]*)\s*$", x_use, description="USE", category="action")
1472
1863
 
1473
1864
  # ------------------------------------------------------------------
1474
1865
  # SYSTEM_CMD
@@ -1476,12 +1867,19 @@ def build_dispatch_table() -> MetaCommandList:
1476
1867
  mcl.add(
1477
1868
  r"^\s*SYSTEM_CMD\s*\(\s*(?P<command>.+)\s*\)(?:\s+(?P<continue>CONTINUE))?\s*$",
1478
1869
  x_system_cmd,
1870
+ description="SYSTEM_CMD",
1871
+ category="action",
1479
1872
  )
1480
1873
 
1481
1874
  # ------------------------------------------------------------------
1482
1875
  # INCLUDE
1483
1876
  # ------------------------------------------------------------------
1484
- mcl.add(ins_fn_rxs(r"^\s*INCLUDE(?P<exists>\s+IF\s+EXISTS?)?\s+", r"\s*$"), x_include)
1877
+ mcl.add(
1878
+ ins_fn_rxs(r"^\s*INCLUDE(?P<exists>\s+IF\s+EXISTS?)?\s+", r"\s*$"),
1879
+ x_include,
1880
+ description="INCLUDE",
1881
+ category="action",
1882
+ )
1485
1883
 
1486
1884
  # ------------------------------------------------------------------
1487
1885
  # IMPORT (CSV / delimited)
@@ -1508,14 +1906,21 @@ def build_dispatch_table() -> MetaCommandList:
1508
1906
  r'^RM_FILE\s+"(?P<filename>.+)"\s*$',
1509
1907
  ),
1510
1908
  x_rm_file,
1909
+ description="RM_FILE",
1910
+ category="action",
1511
1911
  )
1512
- mcl.add(r"^\s*RM_SUB\s+(?P<match>~?\w+)\s*$", x_rm_sub)
1912
+ mcl.add(r"^\s*RM_SUB\s+(?P<match>~?\w+)\s*$", x_rm_sub, description="RM_SUB", category="action")
1513
1913
 
1514
1914
  # ------------------------------------------------------------------
1515
1915
  # SELECT_SUB / SUBDATA
1516
1916
  # ------------------------------------------------------------------
1517
- mcl.add(r"^\s*SELECT_SUB\s+(?P<datasource>.+)\s*$", x_selectsub)
1518
- mcl.add(r"^\s*SUBDATA\s+(?P<match>[+~]?\w+)\s+(?P<datasource>.+)\s*$", x_subdata)
1917
+ mcl.add(r"^\s*SELECT_SUB\s+(?P<datasource>.+)\s*$", x_selectsub, description="SELECT_SUB", category="action")
1918
+ mcl.add(
1919
+ r"^\s*SUBDATA\s+(?P<match>[+~]?\w+)\s+(?P<datasource>.+)\s*$",
1920
+ x_subdata,
1921
+ description="SUBDATA",
1922
+ category="action",
1923
+ )
1519
1924
 
1520
1925
  # ------------------------------------------------------------------
1521
1926
  # PROMPT ASK (simple)
@@ -1526,6 +1931,8 @@ def build_dispatch_table() -> MetaCommandList:
1526
1931
  r')?(?:\s+HELP\s+"(?P<help>[^"]+)")?\s*$',
1527
1932
  ),
1528
1933
  x_prompt_ask,
1934
+ description="PROMPT ASK",
1935
+ category="prompt",
1529
1936
  )
1530
1937
 
1531
1938
  # ------------------------------------------------------------------
@@ -1537,6 +1944,8 @@ def build_dispatch_table() -> MetaCommandList:
1537
1944
  r"(?:\s+HELP\s+(?P<help>[^\s]+))?(?:\s+(?P<free>FREE))?\s*$",
1538
1945
  ),
1539
1946
  x_prompt,
1947
+ description="PROMPT DISPLAY",
1948
+ category="prompt",
1540
1949
  )
1541
1950
  mcl.add(
1542
1951
  ins_table_rxs(
@@ -1563,11 +1972,53 @@ def build_dispatch_table() -> MetaCommandList:
1563
1972
  # ------------------------------------------------------------------
1564
1973
  # PROMPT MESSAGE (simple message / MSG)
1565
1974
  # ------------------------------------------------------------------
1566
- mcl.add(r'^\s*PROMPT(?:\s+MESSAGE)?\s+"(?P<message>(.|\n)*)"\s*$', x_msg)
1975
+ mcl.add(
1976
+ r'^\s*PROMPT(?:\s+MESSAGE)?\s+"(?P<message>(.|\n)*)"\s*$',
1977
+ x_msg,
1978
+ description="PROMPT MESSAGE",
1979
+ category="prompt",
1980
+ )
1567
1981
 
1568
1982
  # ------------------------------------------------------------------
1569
1983
  # WRITE
1570
1984
  # ------------------------------------------------------------------
1985
+ mcl.add(
1986
+ ins_fn_rxs(
1987
+ r"^\s*WRITE\s+\~(?P<text>([^\~]|\n)*)\~(?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
1988
+ r")?\s*$",
1989
+ ),
1990
+ x_write,
1991
+ description="WRITE",
1992
+ category="action",
1993
+ )
1994
+ mcl.add(
1995
+ ins_fn_rxs(
1996
+ r"^\s*WRITE\s+\#(?P<text>([^\#]|\n)*)\#(?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
1997
+ r")?\s*$",
1998
+ ),
1999
+ x_write,
2000
+ )
2001
+ mcl.add(
2002
+ ins_fn_rxs(
2003
+ r"^\s*WRITE\s+\`(?P<text>([^\`]|\n)*)\`(?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
2004
+ r")?\s*$",
2005
+ ),
2006
+ x_write,
2007
+ )
2008
+ mcl.add(
2009
+ ins_fn_rxs(
2010
+ r"^\s*WRITE\s+\[(?P<text>([^\]]|\n)*)\](?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
2011
+ r")?\s*$",
2012
+ ),
2013
+ x_write,
2014
+ )
2015
+ mcl.add(
2016
+ ins_fn_rxs(
2017
+ r"^\s*WRITE\s+\'(?P<text>([^\']|\n)*)\'(?:(?:\s+(?P<tee>TEE))?\s+TO\s+",
2018
+ r")?\s*$",
2019
+ ),
2020
+ x_write,
2021
+ )
1571
2022
  mcl.add(
1572
2023
  ins_fn_rxs(
1573
2024
  r'^\s*WRITE\s+"(?P<text>([^"]|\n)*)"(?:(?:\s+(?P<tee>TEE))?\s+TO\s+',
@@ -1583,8 +2034,8 @@ def build_dispatch_table() -> MetaCommandList:
1583
2034
  mcl.add(
1584
2035
  r"^\s*SUB\s+(?P<match>[+~]?\w+)\s+(?P<repl>.+)$",
1585
2036
  x_sub,
1586
- "SUB",
1587
- "Define a string to match and a replacement for it.",
2037
+ description="SUB",
2038
+ category="action",
1588
2039
  )
1589
2040
 
1590
2041
  return mcl