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
@@ -65,8 +65,8 @@ def xf_hasrows(**kwargs: Any) -> bool:
65
65
  hdrs, rec = _state.dbs.current().select_data(sql)
66
66
  except ErrInfo:
67
67
  raise
68
- except Exception:
69
- raise ErrInfo("db", sql, exception_msg=exception_desc())
68
+ except Exception as e:
69
+ raise ErrInfo("db", sql, exception_msg=exception_desc()) from e
70
70
  nrows = rec[0][0]
71
71
  return nrows > 0
72
72
 
@@ -180,12 +180,12 @@ def xf_iszero(**kwargs: Any) -> bool:
180
180
  val = kwargs["value"].strip()
181
181
  try:
182
182
  v = float(val)
183
- except Exception:
183
+ except Exception as e:
184
184
  raise ErrInfo(
185
185
  type="cmd",
186
186
  command_text=kwargs["metacommandline"],
187
187
  other_msg=f"The value {{{val}}} is not numeric.",
188
- )
188
+ ) from e
189
189
  return v == 0
190
190
 
191
191
 
@@ -195,12 +195,12 @@ def xf_isgt(**kwargs: Any) -> bool:
195
195
  try:
196
196
  v1 = float(val1)
197
197
  v2 = float(val2)
198
- except Exception:
198
+ except Exception as e:
199
199
  raise ErrInfo(
200
200
  type="cmd",
201
201
  command_text=kwargs["metacommandline"],
202
202
  other_msg=f"Values {{{val1}}} and {{{val2}}} are not both numeric.",
203
- )
203
+ ) from e
204
204
  return v1 > v2
205
205
 
206
206
 
@@ -210,12 +210,12 @@ def xf_isgte(**kwargs: Any) -> bool:
210
210
  try:
211
211
  v1 = float(val1)
212
212
  v2 = float(val2)
213
- except Exception:
213
+ except Exception as e:
214
214
  raise ErrInfo(
215
215
  type="cmd",
216
216
  command_text=kwargs["metacommandline"],
217
217
  other_msg=f"Values {{{val1}}} and {{{val2}}} are not both numeric.",
218
- )
218
+ ) from e
219
219
  return v1 >= v2
220
220
 
221
221
 
@@ -291,6 +291,8 @@ def build_conditional_table() -> Any:
291
291
  mcl.add(
292
292
  r"^\s*CONTAINS\s*\(\s*(?P<string1>[^ )]+)\s*,\s*(?P<string2>[^ )]+)(?:\s*,\s*(?P<ignorecase>I))?\s*\)",
293
293
  xf_contains,
294
+ description="CONTAINS",
295
+ category="condition",
294
296
  )
295
297
  mcl.add(
296
298
  r'^\s*CONTAINS\s*\(\s*"(?P<string1>[^"]+)"\s*,\s*(?P<string2>[^ )]+)(?:\s*,\s*(?P<ignorecase>I))?\s*\)',
@@ -357,6 +359,8 @@ def build_conditional_table() -> Any:
357
359
  mcl.add(
358
360
  r"^\s*STARTS_WITH\s*\(\s*(?P<string1>[^ )]+)\s*,\s*(?P<string2>[^ )]+)(?:\s*,\s*(?P<ignorecase>I))?\s*\)",
359
361
  xf_startswith,
362
+ description="STARTS_WITH",
363
+ category="condition",
360
364
  )
361
365
  mcl.add(
362
366
  r'^\s*STARTS_WITH\s*\(\s*"(?P<string1>[^"]+)"\s*,\s*(?P<string2>[^ )]+)(?:\s*,\s*(?P<ignorecase>I))?\s*\)',
@@ -423,6 +427,8 @@ def build_conditional_table() -> Any:
423
427
  mcl.add(
424
428
  r"^\s*ENDS_WITH\s*\(\s*(?P<string1>[^ )]+)\s*,\s*(?P<string2>[^ )]+)(?:\s*,\s*(?P<ignorecase>I))?\s*\)",
425
429
  xf_endswith,
430
+ description="ENDS_WITH",
431
+ category="condition",
426
432
  )
427
433
  mcl.add(
428
434
  r'^\s*ENDS_WITH\s*\(\s*"(?P<string1>[^"]+)"\s*,\s*(?P<string2>[^ )]+)(?:\s*,\s*(?P<ignorecase>I))?\s*\)',
@@ -486,18 +492,23 @@ def build_conditional_table() -> Any:
486
492
  )
487
493
 
488
494
  # HASROWS / HAS_ROWS
489
- mcl.add(r"^\s*HASROWS\((?P<queryname>[^)]+)\)", xf_hasrows)
495
+ mcl.add(r"^\s*HASROWS\((?P<queryname>[^)]+)\)", xf_hasrows, description="HASROWS", category="condition")
490
496
  mcl.add(r"^\s*HAS_ROWS\((?P<queryname>[^)]+)\)", xf_hasrows)
491
497
 
492
498
  # Status predicates
493
- mcl.add(r"^\s*sql_error\(\s*\)", xf_sqlerror)
494
- mcl.add(r"^\s*dialog_canceled\(\s*\)", xf_dialogcanceled)
495
- mcl.add(r"^\s*metacommand_error\(\s*\)", xf_metacommanderror)
496
- mcl.add(r"^\s*CONSOLE_ON", xf_console)
499
+ mcl.add(r"^\s*sql_error\(\s*\)", xf_sqlerror, description="SQL_ERROR", category="condition")
500
+ mcl.add(r"^\s*dialog_canceled\(\s*\)", xf_dialogcanceled, description="DIALOG_CANCELED", category="condition")
501
+ mcl.add(r"^\s*metacommand_error\(\s*\)", xf_metacommanderror, description="METACOMMAND_ERROR", category="condition")
502
+ mcl.add(r"^\s*CONSOLE_ON", xf_console, description="CONSOLE_ON", category="condition")
497
503
 
498
504
  # FILE / DIRECTORY
499
- mcl.add(ins_fn_rxs(r"^FILE_EXISTS\(\s*", r"\)"), xf_fileexists)
500
- mcl.add(r'^DIRECTORY_EXISTS\(\s*("?)(?P<dirname>[^")]+)\1\)', xf_direxists)
505
+ mcl.add(ins_fn_rxs(r"^FILE_EXISTS\(\s*", r"\)"), xf_fileexists, description="FILE_EXISTS", category="condition")
506
+ mcl.add(
507
+ r'^DIRECTORY_EXISTS\(\s*("?)(?P<dirname>[^")]+)\1\)',
508
+ xf_direxists,
509
+ description="DIRECTORY_EXISTS",
510
+ category="condition",
511
+ )
501
512
 
502
513
  # Database object existence
503
514
  mcl.add(
@@ -506,6 +517,8 @@ def build_conditional_table() -> Any:
506
517
  r'^SCHEMA_EXISTS\(\s*"(?P<schema>[A-Za-z0-9_\-\: ]+)"\s*\)',
507
518
  ),
508
519
  xf_schemaexists,
520
+ description="SCHEMA_EXISTS",
521
+ category="condition",
509
522
  )
510
523
  mcl.add(
511
524
  (
@@ -515,6 +528,8 @@ def build_conditional_table() -> Any:
515
528
  r"^TABLE_EXISTS\(\s*(?:(?P<schema>[A-Za-z0-9_\-\/]+)\.)?(?P<tablename>[A-Za-z0-9_\-\/]+)\)",
516
529
  ),
517
530
  xf_tableexists,
531
+ description="TABLE_EXISTS",
532
+ category="condition",
518
533
  )
519
534
  mcl.add(
520
535
  (
@@ -522,8 +537,15 @@ def build_conditional_table() -> Any:
522
537
  r'^ROLE_EXISTS\(\s*"(?P<role>[A-Za-z0-9_\-\:\$ ]+)"\s*\)',
523
538
  ),
524
539
  xf_roleexists,
540
+ description="ROLE_EXISTS",
541
+ category="condition",
542
+ )
543
+ mcl.add(
544
+ r'^\s*VIEW_EXISTS\(\s*("?)(?P<viewname>[^")]+)\1\)',
545
+ xf_viewexists,
546
+ description="VIEW_EXISTS",
547
+ category="condition",
525
548
  )
526
- mcl.add(r'^\s*VIEW_EXISTS\(\s*("?)(?P<viewname>[^")]+)\1\)', xf_viewexists)
527
549
  mcl.add(
528
550
  (
529
551
  r"^COLUMN_EXISTS\(\s*(?P<columnname>[A-Za-z0-9_\-\:]+)\s+IN\s+(?:(?P<schema>[A-Za-z0-9_\-\: ]+)\.)?(?P<tablename>[A-Za-z0-9_\-\: ]+)\)",
@@ -534,28 +556,70 @@ def build_conditional_table() -> Any:
534
556
  r'^COLUMN_EXISTS\(\s*"(?P<columnname>[A-Za-z0-9_\-\: ]+)"\s+IN\s+(?:"(?P<schema>[A-Za-z0-9_\-\: ]+)"\.)?"(?P<tablename>[A-Za-z0-9_\-\: ]+)"\)',
535
557
  ),
536
558
  xf_columnexists,
559
+ description="COLUMN_EXISTS",
560
+ category="condition",
561
+ )
562
+ mcl.add(
563
+ r"^\s*ALIAS_DEFINED\s*\(\s*(?P<alias>\w+)\s*\)",
564
+ xf_aliasdefined,
565
+ description="ALIAS_DEFINED",
566
+ category="condition",
537
567
  )
538
- mcl.add(r"^\s*ALIAS_DEFINED\s*\(\s*(?P<alias>\w+)\s*\)", xf_aliasdefined)
539
568
 
540
569
  # Substitution variable predicates
541
- mcl.add(r"^SUB_DEFINED\s*\(\s*(?P<match_str>[\$&@~#]?\w+)\s*\)", xf_sub_defined)
542
- mcl.add(r"^SUB_EMPTY\s*\(\s*(?P<match_str>[\$&@~#]?\w+)\s*\)", xf_sub_empty)
543
- mcl.add(r"^\s*SCRIPT_EXISTS\s*\(\s*(?P<script_id>\w+)\s*\)", xf_script_exists)
570
+ mcl.add(
571
+ r"^SUB_DEFINED\s*\(\s*(?P<match_str>[\$&@~#]?\w+)\s*\)",
572
+ xf_sub_defined,
573
+ description="SUB_DEFINED",
574
+ category="condition",
575
+ )
576
+ mcl.add(
577
+ r"^SUB_EMPTY\s*\(\s*(?P<match_str>[\$&@~#]?\w+)\s*\)",
578
+ xf_sub_empty,
579
+ description="SUB_EMPTY",
580
+ category="condition",
581
+ )
582
+ mcl.add(
583
+ r"^\s*SCRIPT_EXISTS\s*\(\s*(?P<script_id>\w+)\s*\)",
584
+ xf_script_exists,
585
+ description="SCRIPT_EXISTS",
586
+ category="condition",
587
+ )
544
588
 
545
589
  # Comparison predicates
546
- mcl.add(r"^\s*EQUAL(S)?\s*\(\s*(?P<string1>[^ )]+)\s*,\s*(?P<string2>[^ )]+)\s*\)", xf_equals)
590
+ mcl.add(
591
+ r"^\s*EQUAL(S)?\s*\(\s*(?P<string1>[^ )]+)\s*,\s*(?P<string2>[^ )]+)\s*\)",
592
+ xf_equals,
593
+ description="EQUAL",
594
+ category="condition",
595
+ )
547
596
  mcl.add(r'^\s*EQUAL(S)?\s*\(\s*"(?P<string1>[^"]+)"\s*,\s*(?P<string2>[^ )]+)\s*\)', xf_equals)
548
597
  mcl.add(r'^\s*EQUAL(S)?\s*\(\s*(?P<string1>[^ )]+)\s*,\s*"(?P<string2>[^"]+)"\s*\)', xf_equals)
549
598
  mcl.add(r'^\s*EQUAL(S)?\s*\(\s*"(?P<string1>[^"]+)"\s*,\s*"(?P<string2>[^"]+)"\s*\)', xf_equals)
550
- mcl.add(r"^\s*IDENTICAL\s*\(\s*(?P<string1>[^ ,)]+)\s*,\s*(?P<string2>[^ )]+)\s*\)", xf_identical)
599
+ mcl.add(
600
+ r"^\s*IDENTICAL\s*\(\s*(?P<string1>[^ ,)]+)\s*,\s*(?P<string2>[^ )]+)\s*\)",
601
+ xf_identical,
602
+ description="IDENTICAL",
603
+ category="condition",
604
+ )
551
605
  mcl.add(r'^\s*IDENTICAL\s*\(\s*"(?P<string1>[^"]+)"\s*,\s*(?P<string2>[^ )]+)\s*\)', xf_identical)
552
606
  mcl.add(r'^\s*IDENTICAL\s*\(\s*(?P<string1>[^ ,]+)\s*,\s*"(?P<string2>[^"]+)"\s*\)', xf_identical)
553
607
  mcl.add(r'^\s*IDENTICAL\s*\(\s*"(?P<string1>[^"]+)"\s*,\s*"(?P<string2>[^"]+)"\s*\)', xf_identical)
554
- mcl.add(r'^\s*IS_NULL\(\s*(?P<item>"[^"]*")\s*\)', xf_isnull)
555
- mcl.add(r"^\s*IS_ZERO\(\s*(?P<value>[^)]*)\s*\)", xf_iszero)
556
- mcl.add(r"^\s*IS_GT\(\s*(?P<value1>[^)]*)\s*,\s*(?P<value2>[^)]*)\s*\)", xf_isgt)
557
- mcl.add(r"^\s*IS_GTE\(\s*(?P<value1>[^)]*)\s*,\s*(?P<value2>[^)]*)\s*\)", xf_isgte)
558
- mcl.add(r"^\s*IS_TRUE\(\s*(?P<value>[^)]*)\s*\)", xf_istrue)
608
+ mcl.add(r'^\s*IS_NULL\(\s*(?P<item>"[^"]*")\s*\)', xf_isnull, description="IS_NULL", category="condition")
609
+ mcl.add(r"^\s*IS_ZERO\(\s*(?P<value>[^)]*)\s*\)", xf_iszero, description="IS_ZERO", category="condition")
610
+ mcl.add(
611
+ r"^\s*IS_GT\(\s*(?P<value1>[^)]*)\s*,\s*(?P<value2>[^)]*)\s*\)",
612
+ xf_isgt,
613
+ description="IS_GT",
614
+ category="condition",
615
+ )
616
+ mcl.add(
617
+ r"^\s*IS_GTE\(\s*(?P<value1>[^)]*)\s*,\s*(?P<value2>[^)]*)\s*\)",
618
+ xf_isgte,
619
+ description="IS_GTE",
620
+ category="condition",
621
+ )
622
+ mcl.add(r"^\s*IS_TRUE\(\s*(?P<value>[^)]*)\s*\)", xf_istrue, description="IS_TRUE", category="condition")
559
623
 
560
624
  # Boolean literals
561
625
  mcl.add(
@@ -574,6 +638,8 @@ def build_conditional_table() -> Any:
574
638
  r"^\s*(?P<value>True)\s*",
575
639
  ),
576
640
  xf_boolliteral,
641
+ description="IS_TRUE",
642
+ category="condition",
577
643
  )
578
644
 
579
645
  # Database type / name
@@ -583,6 +649,8 @@ def build_conditional_table() -> Any:
583
649
  r'^\s*DBMS\(\s*"(?P<dbms>[A-Z0-9_\-\(\)\/\\\. ]+)"\s*\)',
584
650
  ),
585
651
  xf_dbms,
652
+ description="DBMS",
653
+ category="condition",
586
654
  )
587
655
  mcl.add(
588
656
  (
@@ -590,6 +658,8 @@ def build_conditional_table() -> Any:
590
658
  r'^\s*DATABASE_NAME\(\s*"(?P<dbname>[A-Z0-9_;\-\(\)\/\\\. ]+)"\s*\)',
591
659
  ),
592
660
  xf_dbname,
661
+ description="DATABASE_NAME",
662
+ category="condition",
593
663
  )
594
664
 
595
665
  # File modification time comparisons
@@ -600,6 +670,8 @@ def build_conditional_table() -> Any:
600
670
  symbolicname="file1",
601
671
  ),
602
672
  xf_newer_file,
673
+ description="NEWER_FILE",
674
+ category="condition",
603
675
  )
604
676
  mcl.add(
605
677
  ins_fn_rxs(
@@ -608,6 +680,8 @@ def build_conditional_table() -> Any:
608
680
  symbolicname="file1",
609
681
  ),
610
682
  xf_newer_date,
683
+ description="NEWER_DATE",
684
+ category="condition",
611
685
  )
612
686
 
613
687
  return mcl
@@ -119,8 +119,10 @@ def x_halt(**kwargs: Any) -> None:
119
119
  if outf:
120
120
  check_dir(outf)
121
121
  of = EncodedFile(outf, conf.output_encoding).open("a")
122
- of.write(f"{errmsg}\n")
123
- of.close()
122
+ try:
123
+ of.write(f"{errmsg}\n")
124
+ finally:
125
+ of.close()
124
126
  if conf.tee_write_log:
125
127
  _state.exec_log.log_user_msg(errmsg)
126
128
  use_gui = gui_console_isrunning()
@@ -219,8 +221,10 @@ def x_halt_msg(**kwargs: Any) -> None:
219
221
  if outf:
220
222
  check_dir(outf)
221
223
  of = EncodedFile(outf, conf.output_encoding).open("a")
222
- of.write(f"{errmsg}\n")
223
- of.close()
224
+ try:
225
+ of.write(f"{errmsg}\n")
226
+ finally:
227
+ of.close()
224
228
  schema = kwargs.get("schema")
225
229
  table = kwargs.get("table")
226
230
  if table:
@@ -123,12 +123,12 @@ def x_subdata(**kwargs: Any) -> None:
123
123
  _, rec = db.select_rowsource(sql)
124
124
  except ErrInfo:
125
125
  raise
126
- except Exception:
126
+ except Exception as e:
127
127
  raise ErrInfo(
128
128
  type="exception",
129
129
  exception_msg=exception_desc(),
130
130
  other_msg=f"Can't get headers and rows from {sql}.",
131
- )
131
+ ) from e
132
132
  try:
133
133
  row1 = next(rec)
134
134
  except Exception:
@@ -155,12 +155,12 @@ def x_selectsub(**kwargs: Any) -> None:
155
155
  hdrs, rec = db.select_rowsource(sql)
156
156
  except ErrInfo:
157
157
  raise
158
- except Exception:
158
+ except Exception as e:
159
159
  raise ErrInfo(
160
160
  type="exception",
161
161
  exception_msg=exception_desc(),
162
162
  other_msg=f"Can't get headers and rows from {sql}.",
163
- )
163
+ ) from e
164
164
  for subvar in hdrs:
165
165
  subvar = "@" + subvar
166
166
  if _state.subvars.sub_exists(subvar):
@@ -171,8 +171,8 @@ def x_selectsub(**kwargs: Any) -> None:
171
171
  row1 = next(rec)
172
172
  except StopIteration:
173
173
  row1 = None
174
- except Exception:
175
- raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=nodatamsg)
174
+ except Exception as e:
175
+ raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=nodatamsg) from e
176
176
  if row1:
177
177
  for i, item in enumerate(row1):
178
178
  if item is None:
@@ -21,8 +21,12 @@ def x_debug_write_metacommands(**kwargs: Any) -> None:
21
21
  ofile = _state.output
22
22
  else:
23
23
  ofile = EncodedFile(output_dest, _state.conf.output_encoding).open("w")
24
- for m in _state.metacommandlist:
25
- ofile.write(f"({m.hitcount}) {m.rx.pattern}\n")
24
+ try:
25
+ for m in _state.metacommandlist:
26
+ ofile.write(f"({m.hitcount}) {m.rx.pattern}\n")
27
+ finally:
28
+ if output_dest is not None and output_dest != "stdout":
29
+ ofile.close()
26
30
 
27
31
 
28
32
  def x_debug_commandliststack(**kwargs: Any) -> None: