tigerbeetle 0.0.34 → 0.0.37

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/ext/tb_client/extconf.rb +13 -13
  4. data/ext/tb_client/tigerbeetle/LICENSE +177 -0
  5. data/ext/tb_client/tigerbeetle/build.zig +2327 -0
  6. data/ext/tb_client/tigerbeetle/src/aof.zig +1000 -0
  7. data/ext/tb_client/tigerbeetle/src/build_multiversion.zig +808 -0
  8. data/ext/tb_client/tigerbeetle/src/cdc/amqp/protocol.zig +1283 -0
  9. data/ext/tb_client/tigerbeetle/src/cdc/amqp/spec.zig +1704 -0
  10. data/ext/tb_client/tigerbeetle/src/cdc/amqp/types.zig +341 -0
  11. data/ext/tb_client/tigerbeetle/src/cdc/amqp.zig +1450 -0
  12. data/ext/tb_client/tigerbeetle/src/cdc/runner.zig +1659 -0
  13. data/ext/tb_client/tigerbeetle/src/clients/c/samples/main.c +406 -0
  14. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/context.zig +1084 -0
  15. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/echo_client.zig +286 -0
  16. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/packet.zig +158 -0
  17. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal.zig +229 -0
  18. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal_fuzz.zig +110 -0
  19. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.h +386 -0
  20. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.zig +34 -0
  21. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_exports.zig +281 -0
  22. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header.zig +312 -0
  23. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header_test.zig +138 -0
  24. data/ext/tb_client/tigerbeetle/src/clients/c/test.zig +466 -0
  25. data/ext/tb_client/tigerbeetle/src/clients/docs_samples.zig +157 -0
  26. data/ext/tb_client/tigerbeetle/src/clients/docs_types.zig +90 -0
  27. data/ext/tb_client/tigerbeetle/src/clients/dotnet/ci.zig +203 -0
  28. data/ext/tb_client/tigerbeetle/src/clients/dotnet/docs.zig +79 -0
  29. data/ext/tb_client/tigerbeetle/src/clients/dotnet/dotnet_bindings.zig +542 -0
  30. data/ext/tb_client/tigerbeetle/src/clients/go/ci.zig +109 -0
  31. data/ext/tb_client/tigerbeetle/src/clients/go/docs.zig +86 -0
  32. data/ext/tb_client/tigerbeetle/src/clients/go/go_bindings.zig +370 -0
  33. data/ext/tb_client/tigerbeetle/src/clients/go/pkg/native/tb_client.h +386 -0
  34. data/ext/tb_client/tigerbeetle/src/clients/java/ci.zig +167 -0
  35. data/ext/tb_client/tigerbeetle/src/clients/java/docs.zig +126 -0
  36. data/ext/tb_client/tigerbeetle/src/clients/java/java_bindings.zig +996 -0
  37. data/ext/tb_client/tigerbeetle/src/clients/java/src/client.zig +748 -0
  38. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni.zig +3238 -0
  39. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_tests.zig +1718 -0
  40. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_thread_cleaner.zig +190 -0
  41. data/ext/tb_client/tigerbeetle/src/clients/node/ci.zig +104 -0
  42. data/ext/tb_client/tigerbeetle/src/clients/node/docs.zig +75 -0
  43. data/ext/tb_client/tigerbeetle/src/clients/node/node.zig +522 -0
  44. data/ext/tb_client/tigerbeetle/src/clients/node/node_bindings.zig +267 -0
  45. data/ext/tb_client/tigerbeetle/src/clients/node/src/c.zig +3 -0
  46. data/ext/tb_client/tigerbeetle/src/clients/node/src/translate.zig +379 -0
  47. data/ext/tb_client/tigerbeetle/src/clients/python/ci.zig +131 -0
  48. data/ext/tb_client/tigerbeetle/src/clients/python/docs.zig +63 -0
  49. data/ext/tb_client/tigerbeetle/src/clients/python/python_bindings.zig +588 -0
  50. data/ext/tb_client/tigerbeetle/src/clients/rust/assets/tb_client.h +386 -0
  51. data/ext/tb_client/tigerbeetle/src/clients/rust/ci.zig +73 -0
  52. data/ext/tb_client/tigerbeetle/src/clients/rust/docs.zig +106 -0
  53. data/ext/tb_client/tigerbeetle/src/clients/rust/rust_bindings.zig +305 -0
  54. data/ext/tb_client/tigerbeetle/src/config.zig +296 -0
  55. data/ext/tb_client/tigerbeetle/src/constants.zig +790 -0
  56. data/ext/tb_client/tigerbeetle/src/copyhound.zig +202 -0
  57. data/ext/tb_client/tigerbeetle/src/counting_allocator.zig +72 -0
  58. data/ext/tb_client/tigerbeetle/src/direction.zig +11 -0
  59. data/ext/tb_client/tigerbeetle/src/docs_website/build.zig +158 -0
  60. data/ext/tb_client/tigerbeetle/src/docs_website/src/content.zig +156 -0
  61. data/ext/tb_client/tigerbeetle/src/docs_website/src/docs.zig +252 -0
  62. data/ext/tb_client/tigerbeetle/src/docs_website/src/file_checker.zig +313 -0
  63. data/ext/tb_client/tigerbeetle/src/docs_website/src/html.zig +87 -0
  64. data/ext/tb_client/tigerbeetle/src/docs_website/src/page_writer.zig +63 -0
  65. data/ext/tb_client/tigerbeetle/src/docs_website/src/redirects.zig +47 -0
  66. data/ext/tb_client/tigerbeetle/src/docs_website/src/search_index_writer.zig +28 -0
  67. data/ext/tb_client/tigerbeetle/src/docs_website/src/service_worker_writer.zig +61 -0
  68. data/ext/tb_client/tigerbeetle/src/docs_website/src/single_page_writer.zig +169 -0
  69. data/ext/tb_client/tigerbeetle/src/docs_website/src/website.zig +46 -0
  70. data/ext/tb_client/tigerbeetle/src/ewah.zig +445 -0
  71. data/ext/tb_client/tigerbeetle/src/ewah_benchmark.zig +128 -0
  72. data/ext/tb_client/tigerbeetle/src/ewah_fuzz.zig +171 -0
  73. data/ext/tb_client/tigerbeetle/src/fuzz_tests.zig +179 -0
  74. data/ext/tb_client/tigerbeetle/src/integration_tests.zig +662 -0
  75. data/ext/tb_client/tigerbeetle/src/io/common.zig +155 -0
  76. data/ext/tb_client/tigerbeetle/src/io/darwin.zig +1093 -0
  77. data/ext/tb_client/tigerbeetle/src/io/linux.zig +1880 -0
  78. data/ext/tb_client/tigerbeetle/src/io/test.zig +1005 -0
  79. data/ext/tb_client/tigerbeetle/src/io/windows.zig +1598 -0
  80. data/ext/tb_client/tigerbeetle/src/io.zig +34 -0
  81. data/ext/tb_client/tigerbeetle/src/iops.zig +134 -0
  82. data/ext/tb_client/tigerbeetle/src/list.zig +236 -0
  83. data/ext/tb_client/tigerbeetle/src/lsm/binary_search.zig +848 -0
  84. data/ext/tb_client/tigerbeetle/src/lsm/binary_search_benchmark.zig +179 -0
  85. data/ext/tb_client/tigerbeetle/src/lsm/cache_map.zig +424 -0
  86. data/ext/tb_client/tigerbeetle/src/lsm/cache_map_fuzz.zig +420 -0
  87. data/ext/tb_client/tigerbeetle/src/lsm/compaction.zig +2117 -0
  88. data/ext/tb_client/tigerbeetle/src/lsm/composite_key.zig +182 -0
  89. data/ext/tb_client/tigerbeetle/src/lsm/forest.zig +1119 -0
  90. data/ext/tb_client/tigerbeetle/src/lsm/forest_fuzz.zig +1102 -0
  91. data/ext/tb_client/tigerbeetle/src/lsm/forest_table_iterator.zig +200 -0
  92. data/ext/tb_client/tigerbeetle/src/lsm/groove.zig +1495 -0
  93. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge.zig +739 -0
  94. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge_benchmark.zig +166 -0
  95. data/ext/tb_client/tigerbeetle/src/lsm/manifest.zig +754 -0
  96. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level.zig +1294 -0
  97. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level_fuzz.zig +510 -0
  98. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log.zig +1263 -0
  99. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log_fuzz.zig +628 -0
  100. data/ext/tb_client/tigerbeetle/src/lsm/node_pool.zig +247 -0
  101. data/ext/tb_client/tigerbeetle/src/lsm/scan_buffer.zig +116 -0
  102. data/ext/tb_client/tigerbeetle/src/lsm/scan_builder.zig +543 -0
  103. data/ext/tb_client/tigerbeetle/src/lsm/scan_fuzz.zig +938 -0
  104. data/ext/tb_client/tigerbeetle/src/lsm/scan_lookup.zig +293 -0
  105. data/ext/tb_client/tigerbeetle/src/lsm/scan_merge.zig +362 -0
  106. data/ext/tb_client/tigerbeetle/src/lsm/scan_range.zig +99 -0
  107. data/ext/tb_client/tigerbeetle/src/lsm/scan_state.zig +17 -0
  108. data/ext/tb_client/tigerbeetle/src/lsm/scan_tree.zig +1036 -0
  109. data/ext/tb_client/tigerbeetle/src/lsm/schema.zig +617 -0
  110. data/ext/tb_client/tigerbeetle/src/lsm/scratch_memory.zig +84 -0
  111. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array.zig +1500 -0
  112. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_benchmark.zig +149 -0
  113. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_fuzz.zig +7 -0
  114. data/ext/tb_client/tigerbeetle/src/lsm/set_associative_cache.zig +865 -0
  115. data/ext/tb_client/tigerbeetle/src/lsm/table.zig +607 -0
  116. data/ext/tb_client/tigerbeetle/src/lsm/table_memory.zig +843 -0
  117. data/ext/tb_client/tigerbeetle/src/lsm/table_value_iterator.zig +105 -0
  118. data/ext/tb_client/tigerbeetle/src/lsm/timestamp_range.zig +40 -0
  119. data/ext/tb_client/tigerbeetle/src/lsm/tree.zig +630 -0
  120. data/ext/tb_client/tigerbeetle/src/lsm/tree_fuzz.zig +933 -0
  121. data/ext/tb_client/tigerbeetle/src/lsm/zig_zag_merge.zig +557 -0
  122. data/ext/tb_client/tigerbeetle/src/message_buffer.zig +469 -0
  123. data/ext/tb_client/tigerbeetle/src/message_bus.zig +1214 -0
  124. data/ext/tb_client/tigerbeetle/src/message_bus_fuzz.zig +936 -0
  125. data/ext/tb_client/tigerbeetle/src/message_pool.zig +343 -0
  126. data/ext/tb_client/tigerbeetle/src/multiversion.zig +2195 -0
  127. data/ext/tb_client/tigerbeetle/src/queue.zig +390 -0
  128. data/ext/tb_client/tigerbeetle/src/repl/completion.zig +201 -0
  129. data/ext/tb_client/tigerbeetle/src/repl/parser.zig +1356 -0
  130. data/ext/tb_client/tigerbeetle/src/repl/terminal.zig +496 -0
  131. data/ext/tb_client/tigerbeetle/src/repl.zig +1034 -0
  132. data/ext/tb_client/tigerbeetle/src/scripts/amqp.zig +973 -0
  133. data/ext/tb_client/tigerbeetle/src/scripts/cfo.zig +1866 -0
  134. data/ext/tb_client/tigerbeetle/src/scripts/changelog.zig +304 -0
  135. data/ext/tb_client/tigerbeetle/src/scripts/ci.zig +227 -0
  136. data/ext/tb_client/tigerbeetle/src/scripts/client_readmes.zig +658 -0
  137. data/ext/tb_client/tigerbeetle/src/scripts/devhub.zig +466 -0
  138. data/ext/tb_client/tigerbeetle/src/scripts/release.zig +1058 -0
  139. data/ext/tb_client/tigerbeetle/src/scripts.zig +105 -0
  140. data/ext/tb_client/tigerbeetle/src/shell.zig +1195 -0
  141. data/ext/tb_client/tigerbeetle/src/stack.zig +260 -0
  142. data/ext/tb_client/tigerbeetle/src/state_machine/auditor.zig +911 -0
  143. data/ext/tb_client/tigerbeetle/src/state_machine/workload.zig +2079 -0
  144. data/ext/tb_client/tigerbeetle/src/state_machine.zig +4872 -0
  145. data/ext/tb_client/tigerbeetle/src/state_machine_fuzz.zig +288 -0
  146. data/ext/tb_client/tigerbeetle/src/state_machine_tests.zig +3128 -0
  147. data/ext/tb_client/tigerbeetle/src/static_allocator.zig +82 -0
  148. data/ext/tb_client/tigerbeetle/src/stdx/bit_set.zig +157 -0
  149. data/ext/tb_client/tigerbeetle/src/stdx/bounded_array.zig +292 -0
  150. data/ext/tb_client/tigerbeetle/src/stdx/debug.zig +65 -0
  151. data/ext/tb_client/tigerbeetle/src/stdx/flags.zig +1414 -0
  152. data/ext/tb_client/tigerbeetle/src/stdx/mlock.zig +92 -0
  153. data/ext/tb_client/tigerbeetle/src/stdx/prng.zig +677 -0
  154. data/ext/tb_client/tigerbeetle/src/stdx/radix.zig +336 -0
  155. data/ext/tb_client/tigerbeetle/src/stdx/ring_buffer.zig +511 -0
  156. data/ext/tb_client/tigerbeetle/src/stdx/sort_test.zig +112 -0
  157. data/ext/tb_client/tigerbeetle/src/stdx/stdx.zig +1160 -0
  158. data/ext/tb_client/tigerbeetle/src/stdx/testing/low_level_hash_vectors.zig +142 -0
  159. data/ext/tb_client/tigerbeetle/src/stdx/testing/snaptest.zig +361 -0
  160. data/ext/tb_client/tigerbeetle/src/stdx/time_units.zig +275 -0
  161. data/ext/tb_client/tigerbeetle/src/stdx/unshare.zig +295 -0
  162. data/ext/tb_client/tigerbeetle/src/stdx/vendored/aegis.zig +436 -0
  163. data/ext/tb_client/tigerbeetle/src/stdx/windows.zig +48 -0
  164. data/ext/tb_client/tigerbeetle/src/stdx/zipfian.zig +402 -0
  165. data/ext/tb_client/tigerbeetle/src/storage.zig +489 -0
  166. data/ext/tb_client/tigerbeetle/src/storage_fuzz.zig +180 -0
  167. data/ext/tb_client/tigerbeetle/src/testing/bench.zig +146 -0
  168. data/ext/tb_client/tigerbeetle/src/testing/cluster/grid_checker.zig +53 -0
  169. data/ext/tb_client/tigerbeetle/src/testing/cluster/journal_checker.zig +61 -0
  170. data/ext/tb_client/tigerbeetle/src/testing/cluster/manifest_checker.zig +76 -0
  171. data/ext/tb_client/tigerbeetle/src/testing/cluster/message_bus.zig +110 -0
  172. data/ext/tb_client/tigerbeetle/src/testing/cluster/network.zig +412 -0
  173. data/ext/tb_client/tigerbeetle/src/testing/cluster/state_checker.zig +331 -0
  174. data/ext/tb_client/tigerbeetle/src/testing/cluster/storage_checker.zig +458 -0
  175. data/ext/tb_client/tigerbeetle/src/testing/cluster.zig +1198 -0
  176. data/ext/tb_client/tigerbeetle/src/testing/exhaustigen.zig +128 -0
  177. data/ext/tb_client/tigerbeetle/src/testing/fixtures.zig +181 -0
  178. data/ext/tb_client/tigerbeetle/src/testing/fuzz.zig +144 -0
  179. data/ext/tb_client/tigerbeetle/src/testing/id.zig +97 -0
  180. data/ext/tb_client/tigerbeetle/src/testing/io.zig +317 -0
  181. data/ext/tb_client/tigerbeetle/src/testing/marks.zig +126 -0
  182. data/ext/tb_client/tigerbeetle/src/testing/packet_simulator.zig +533 -0
  183. data/ext/tb_client/tigerbeetle/src/testing/reply_sequence.zig +154 -0
  184. data/ext/tb_client/tigerbeetle/src/testing/state_machine.zig +389 -0
  185. data/ext/tb_client/tigerbeetle/src/testing/storage.zig +1247 -0
  186. data/ext/tb_client/tigerbeetle/src/testing/table.zig +249 -0
  187. data/ext/tb_client/tigerbeetle/src/testing/time.zig +98 -0
  188. data/ext/tb_client/tigerbeetle/src/testing/tmp_tigerbeetle.zig +212 -0
  189. data/ext/tb_client/tigerbeetle/src/testing/vortex/constants.zig +26 -0
  190. data/ext/tb_client/tigerbeetle/src/testing/vortex/faulty_network.zig +580 -0
  191. data/ext/tb_client/tigerbeetle/src/testing/vortex/java_driver/ci.zig +39 -0
  192. data/ext/tb_client/tigerbeetle/src/testing/vortex/logged_process.zig +214 -0
  193. data/ext/tb_client/tigerbeetle/src/testing/vortex/rust_driver/ci.zig +34 -0
  194. data/ext/tb_client/tigerbeetle/src/testing/vortex/supervisor.zig +766 -0
  195. data/ext/tb_client/tigerbeetle/src/testing/vortex/workload.zig +543 -0
  196. data/ext/tb_client/tigerbeetle/src/testing/vortex/zig_driver.zig +181 -0
  197. data/ext/tb_client/tigerbeetle/src/tidy.zig +1448 -0
  198. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_driver.zig +227 -0
  199. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_load.zig +1069 -0
  200. data/ext/tb_client/tigerbeetle/src/tigerbeetle/cli.zig +1422 -0
  201. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect.zig +1658 -0
  202. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect_integrity.zig +518 -0
  203. data/ext/tb_client/tigerbeetle/src/tigerbeetle/libtb_client.zig +36 -0
  204. data/ext/tb_client/tigerbeetle/src/tigerbeetle/main.zig +646 -0
  205. data/ext/tb_client/tigerbeetle/src/tigerbeetle.zig +958 -0
  206. data/ext/tb_client/tigerbeetle/src/time.zig +236 -0
  207. data/ext/tb_client/tigerbeetle/src/trace/event.zig +745 -0
  208. data/ext/tb_client/tigerbeetle/src/trace/statsd.zig +462 -0
  209. data/ext/tb_client/tigerbeetle/src/trace.zig +556 -0
  210. data/ext/tb_client/tigerbeetle/src/unit_tests.zig +321 -0
  211. data/ext/tb_client/tigerbeetle/src/vopr.zig +1785 -0
  212. data/ext/tb_client/tigerbeetle/src/vortex.zig +101 -0
  213. data/ext/tb_client/tigerbeetle/src/vsr/checkpoint_trailer.zig +473 -0
  214. data/ext/tb_client/tigerbeetle/src/vsr/checksum.zig +208 -0
  215. data/ext/tb_client/tigerbeetle/src/vsr/checksum_benchmark.zig +43 -0
  216. data/ext/tb_client/tigerbeetle/src/vsr/client.zig +768 -0
  217. data/ext/tb_client/tigerbeetle/src/vsr/client_replies.zig +532 -0
  218. data/ext/tb_client/tigerbeetle/src/vsr/client_sessions.zig +338 -0
  219. data/ext/tb_client/tigerbeetle/src/vsr/clock.zig +1019 -0
  220. data/ext/tb_client/tigerbeetle/src/vsr/fault_detector.zig +279 -0
  221. data/ext/tb_client/tigerbeetle/src/vsr/free_set.zig +1381 -0
  222. data/ext/tb_client/tigerbeetle/src/vsr/free_set_fuzz.zig +315 -0
  223. data/ext/tb_client/tigerbeetle/src/vsr/grid.zig +1460 -0
  224. data/ext/tb_client/tigerbeetle/src/vsr/grid_blocks_missing.zig +757 -0
  225. data/ext/tb_client/tigerbeetle/src/vsr/grid_scrubber.zig +797 -0
  226. data/ext/tb_client/tigerbeetle/src/vsr/journal.zig +2586 -0
  227. data/ext/tb_client/tigerbeetle/src/vsr/marzullo.zig +308 -0
  228. data/ext/tb_client/tigerbeetle/src/vsr/message_header.zig +1777 -0
  229. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch.zig +715 -0
  230. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch_fuzz.zig +185 -0
  231. data/ext/tb_client/tigerbeetle/src/vsr/repair_budget.zig +333 -0
  232. data/ext/tb_client/tigerbeetle/src/vsr/replica.zig +12355 -0
  233. data/ext/tb_client/tigerbeetle/src/vsr/replica_format.zig +416 -0
  234. data/ext/tb_client/tigerbeetle/src/vsr/replica_reformat.zig +165 -0
  235. data/ext/tb_client/tigerbeetle/src/vsr/replica_test.zig +2910 -0
  236. data/ext/tb_client/tigerbeetle/src/vsr/routing.zig +1075 -0
  237. data/ext/tb_client/tigerbeetle/src/vsr/superblock.zig +1603 -0
  238. data/ext/tb_client/tigerbeetle/src/vsr/superblock_fuzz.zig +484 -0
  239. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums.zig +405 -0
  240. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +355 -0
  241. data/ext/tb_client/tigerbeetle/src/vsr/sync.zig +29 -0
  242. data/ext/tb_client/tigerbeetle/src/vsr.zig +1727 -0
  243. data/lib/tb_client/shared_lib.rb +12 -5
  244. data/lib/tigerbeetle/client.rb +1 -1
  245. data/lib/tigerbeetle/platforms.rb +9 -0
  246. data/lib/tigerbeetle/version.rb +2 -2
  247. data/tigerbeetle.gemspec +22 -5
  248. metadata +242 -3
  249. data/ext/tb_client/pkg.tar.gz +0 -0
@@ -0,0 +1,1414 @@
1
+ //! The purpose of `flags` is to define standard behavior for parsing CLI arguments and provide
2
+ //! a specific parsing library, implementing this behavior.
3
+ //!
4
+ //! These are TigerBeetle CLI guidelines:
5
+ //!
6
+ //! - The main principle is robustness --- make operator errors harder to make.
7
+ //! - For production usage, avoid defaults.
8
+ //! - Thoroughly validate options.
9
+ //! - In particular, check that no options are repeated.
10
+ //! - Use only long options (`--addresses`).
11
+ //! - Exception: `-h/--help` is allowed.
12
+ //! - Use `--key=value` syntax for an option with an argument.
13
+ //! Don't use `--key value`, as that can be ambiguous (e.g., `--key --verbose`).
14
+ //! - Use subcommand syntax when appropriate.
15
+ //! - Use positional arguments when appropriate.
16
+ //!
17
+ //! Design choices for this particular `flags` library:
18
+ //!
19
+ //! - Be a 80% solution. Parsing arguments is a surprisingly vast topic: auto-generated help,
20
+ //! bash completions, typo correction. Rather than providing a definitive solution, `flags`
21
+ //! is just one possible option. It is ok to re-implement arg parsing in a different way, as long
22
+ //! as the CLI guidelines are observed.
23
+ //!
24
+ //! - No auto-generated help. Zig doesn't expose doc comments through `@typeInfo`, so its hard to
25
+ //! implement auto-help nicely. Additionally, fully hand-crafted `--help` message can be of
26
+ //! higher quality.
27
+ //!
28
+ //! - Fatal errors. It might be "cleaner" to use `try` to propagate the error to the caller, but
29
+ //! during early CLI parsing, it is much simpler to terminate the process directly and save the
30
+ //! caller the hassle of propagating errors. The `fatal` function is public, to allow the caller
31
+ //! to run additional validation or parsing using the same error reporting mechanism.
32
+ //!
33
+ //! - Concise DSL. Most cli parsing is done for ad-hoc tools like benchmarking, where the ability to
34
+ //! quickly add a new argument is valuable. As this is a 80% solution, production code may use
35
+ //! more verbose approach if it gives better UX.
36
+ //!
37
+ //! - Caller manages ArgsIterator. ArgsIterator owns the backing memory of the args, so we let the
38
+ //! caller to manage the lifetime. The caller should be skipping program name.
39
+
40
+ const std = @import("std");
41
+ const stdx = @import("stdx.zig");
42
+ const builtin = @import("builtin");
43
+ const assert = std.debug.assert;
44
+
45
+ /// Format and print an error message to stderr, then exit with an exit code of 1.
46
+ fn fatal(comptime fmt_string: []const u8, args: anytype) noreturn {
47
+ const stderr = std.io.getStdErr().writer();
48
+ stderr.print("error: " ++ fmt_string ++ "\n", args) catch {};
49
+ // NB: this status must match vsr.FatalReason.cli, but it would be wrong for flags to depend on
50
+ // vsr. The right way would be to parametrize flags by this behavior, and let the caller inject
51
+ // the implementation of fatal function, but let's be pragmatic here and just match the behavior
52
+ // manually.
53
+ std.process.exit(1);
54
+ }
55
+
56
+ /// Parse CLI arguments for subcommands specified as Zig `struct` or `union(enum)`:
57
+ ///
58
+ /// ```
59
+ /// const CLIArgs = union(enum) {
60
+ /// start: struct { addresses: []const u8, replica: u32 },
61
+ /// format: struct {
62
+ /// verbose: bool = false,
63
+ /// @"--": void,
64
+ /// path: []const u8,
65
+ /// },
66
+ ///
67
+ /// pub const help =
68
+ /// \\ tigerbeetle start --addresses=<addresses> --replica=<replica>
69
+ /// \\ tigerbeetle format [--verbose] <path>
70
+ /// }
71
+ ///
72
+ /// const cli_args = parse_commands(&args, CLIArgs);
73
+ /// ```
74
+ ///
75
+ /// `@"--"` field is treated specially, it delineates positional arguments.
76
+ ///
77
+ /// If `pub const help` declaration is present, it is used to implement `-h/--help` argument.
78
+ ///
79
+ /// Value parsing can be customized on per-type basis via `parse_flag_value` customization point.
80
+ pub fn parse(args: *std.process.ArgIterator, comptime CLIArgs: type) CLIArgs {
81
+ comptime assert(CLIArgs != void);
82
+ assert(args.skip()); // Discard executable name.
83
+ return parse_flags(args, CLIArgs);
84
+ }
85
+
86
+ fn parse_commands(args: *std.process.ArgIterator, comptime Commands: type) Commands {
87
+ comptime assert(@typeInfo(Commands) == .@"union");
88
+ comptime assert(std.meta.fields(Commands).len >= 2);
89
+
90
+ const first_arg = args.next() orelse fatal(
91
+ "subcommand required, expected {s}",
92
+ .{comptime fields_to_comma_list(Commands)},
93
+ );
94
+
95
+ // NB: help must be declared as *pub* const to be visible here.
96
+ if (@hasDecl(Commands, "help")) {
97
+ if (std.mem.eql(u8, first_arg, "-h") or std.mem.eql(u8, first_arg, "--help")) {
98
+ std.io.getStdOut().writeAll(Commands.help) catch std.process.exit(1);
99
+ std.process.exit(0);
100
+ }
101
+ }
102
+
103
+ inline for (comptime std.meta.fields(Commands)) |field| {
104
+ comptime assert(std.mem.indexOfScalar(u8, field.name, '_') == null);
105
+ if (std.mem.eql(u8, first_arg, field.name)) {
106
+ return @unionInit(Commands, field.name, parse_flags(args, field.type));
107
+ }
108
+ }
109
+ fatal("unknown subcommand: '{s}'", .{first_arg});
110
+ }
111
+
112
+ fn parse_flags(args: *std.process.ArgIterator, comptime Flags: type) Flags {
113
+ @setEvalBranchQuota(5_000);
114
+
115
+ if (Flags == void) {
116
+ if (args.next()) |arg| {
117
+ fatal("unexpected argument: '{s}'", .{arg});
118
+ }
119
+ return {};
120
+ }
121
+
122
+ if (@typeInfo(Flags) == .@"union") {
123
+ return parse_commands(args, Flags);
124
+ }
125
+
126
+ assert(@typeInfo(Flags) == .@"struct");
127
+
128
+ const fields = std.meta.fields(Flags);
129
+ comptime var fields_named, const fields_positional, const fields_extended =
130
+ for (fields, 0..) |field, index| {
131
+ if (std.mem.eql(u8, field.name, "--")) {
132
+ assert(field.type == void);
133
+ break .{
134
+ fields[0..index].*,
135
+ fields[index + 1 ..].*,
136
+ index == fields.len - 1,
137
+ };
138
+ }
139
+ } else .{
140
+ fields[0..fields.len].*,
141
+ [_]std.builtin.Type.StructField{},
142
+ false,
143
+ };
144
+
145
+ comptime {
146
+ if (fields_positional.len == 0) {
147
+ assert(fields.len == fields_named.len + @intFromBool(fields_extended));
148
+ } else {
149
+ assert(fields.len == fields_named.len + 1 + fields_positional.len);
150
+ assert(!fields_extended);
151
+ }
152
+
153
+ // When parsing named arguments, we must consider longer arguments first, such that
154
+ // `--foo-bar=92` is not confused for a misspelled `--foo=92`. Using `std.sort` for
155
+ // comptime-only values does not work, so open-code insertion sort, and comptime assert
156
+ // order during the actual parsing.
157
+ for (fields_named[0..], 0..) |*field_right, i| {
158
+ for (fields_named[0..i]) |*field_left| {
159
+ if (field_left.name.len < field_right.name.len) {
160
+ std.mem.swap(std.builtin.Type.StructField, field_left, field_right);
161
+ }
162
+ }
163
+ }
164
+
165
+ for (fields_named) |field| {
166
+ switch (@typeInfo(field.type)) {
167
+ .bool => {
168
+ // Boolean flags must have a default.
169
+ assert(field.defaultValue() != null);
170
+ assert(field.defaultValue().? == false);
171
+ },
172
+ .optional => |optional| {
173
+ // Optional flags must have a default.
174
+ assert(field.defaultValue() != null);
175
+ assert(field.defaultValue().? == null);
176
+
177
+ assert_valid_value_type(optional.child);
178
+ },
179
+ else => {
180
+ assert_valid_value_type(field.type);
181
+ },
182
+ }
183
+ }
184
+
185
+ var optional_tail: bool = false;
186
+ for (fields_positional) |field| {
187
+ if (field.defaultValue() == null) {
188
+ if (optional_tail) @panic("optional positional arguments must be trailing");
189
+ } else {
190
+ optional_tail = true;
191
+ }
192
+ switch (@typeInfo(field.type)) {
193
+ .optional => |optional| {
194
+ // optional flags should have a default
195
+ assert(field.defaultValue() != null);
196
+ assert(field.defaultValue().? == null);
197
+ assert_valid_value_type(optional.child);
198
+ },
199
+ else => {
200
+ assert_valid_value_type(field.type);
201
+ },
202
+ }
203
+ }
204
+ }
205
+
206
+ var counts: std.enums.EnumFieldStruct(std.meta.FieldEnum(Flags), u32, 0) = .{};
207
+ var result: Flags = undefined;
208
+ var parsed_positional = false;
209
+ next_arg: while (args.next()) |arg| {
210
+ comptime var field_len_prev = std.math.maxInt(usize);
211
+ inline for (fields_named) |field| {
212
+ const flag = comptime flag_name(field);
213
+
214
+ comptime assert(field_len_prev >= field.name.len);
215
+ field_len_prev = field.name.len;
216
+ if (std.mem.startsWith(u8, arg, flag)) {
217
+ if (parsed_positional) {
218
+ fatal("unexpected trailing option: '{s}'", .{arg});
219
+ }
220
+
221
+ @field(counts, field.name) += 1;
222
+ const flag_value = parse_flag(field.type, flag, arg);
223
+ @field(result, field.name) = flag_value;
224
+ continue :next_arg;
225
+ }
226
+ }
227
+
228
+ if (fields_positional.len > 0) {
229
+ assert(!fields_extended);
230
+ counts.@"--" += 1;
231
+ switch (counts.@"--" - 1) {
232
+ inline 0...fields_positional.len - 1 => |field_index| {
233
+ const field = fields_positional[field_index];
234
+ const flag = comptime flag_name_positional(field);
235
+
236
+ if (arg.len == 0) fatal("{s}: empty argument", .{flag});
237
+ // Prevent ambiguity between a flag and positional argument value. We could add
238
+ // support for bare ` -- ` as a disambiguation mechanism once we have a real
239
+ // use-case.
240
+ if (arg[0] == '-') fatal("unexpected argument: '{s}'", .{arg});
241
+ parsed_positional = true;
242
+
243
+ @field(result, field.name) =
244
+ parse_value(field.type, flag, arg);
245
+ continue :next_arg;
246
+ },
247
+ else => {}, // Fall-through to the unexpected argument error.
248
+ }
249
+ } else {
250
+ if (fields_extended) {
251
+ if (std.mem.eql(u8, arg, "--")) {
252
+ break;
253
+ } else {
254
+ fatal("unexpected argument: '{s}'; expected '-- ...'", .{arg});
255
+ }
256
+ }
257
+ }
258
+
259
+ fatal("unexpected argument: '{s}'", .{arg});
260
+ }
261
+ if (!fields_extended) assert(args.next() == null);
262
+
263
+ inline for (fields_named) |field| {
264
+ const flag = flag_name(field);
265
+ switch (@field(counts, field.name)) {
266
+ 0 => if (field.defaultValue()) |default| {
267
+ @field(result, field.name) = default;
268
+ } else {
269
+ fatal("{s}: argument is required", .{flag});
270
+ },
271
+ 1 => {},
272
+ else => fatal("{s}: duplicate argument", .{flag}),
273
+ }
274
+ }
275
+
276
+ if (fields_positional.len > 0) {
277
+ assert(counts.@"--" <= fields_positional.len);
278
+ inline for (fields_positional, 0..) |field, field_index| {
279
+ if (field_index >= counts.@"--") {
280
+ const flag = comptime flag_name_positional(field);
281
+ if (field.defaultValue()) |default| {
282
+ @field(result, field.name) = default;
283
+ } else {
284
+ fatal("{s}: argument is required", .{flag});
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ return result;
291
+ }
292
+
293
+ fn assert_valid_value_type(comptime T: type) void {
294
+ comptime {
295
+ if (T == []const u8 or T == [:0]const u8 or @typeInfo(T) == .int) return;
296
+ if (@hasDecl(T, "parse_flag_value")) return;
297
+
298
+ if (@typeInfo(T) == .@"enum") {
299
+ const info = @typeInfo(T).@"enum";
300
+ assert(info.is_exhaustive);
301
+ assert(info.fields.len >= 2);
302
+ return;
303
+ }
304
+
305
+ @compileError("flags: unsupported type: " ++ @typeName(T));
306
+ }
307
+ }
308
+
309
+ /// Parse, e.g., `--cluster=123` into `123` integer
310
+ fn parse_flag(comptime T: type, flag: []const u8, arg: [:0]const u8) T {
311
+ assert(flag[0] == '-' and flag[1] == '-');
312
+
313
+ if (T == bool) {
314
+ if (std.mem.eql(u8, arg, flag)) {
315
+ // Bool argument may not have a value.
316
+ return true;
317
+ }
318
+ }
319
+
320
+ const value = parse_flag_split_value(flag, arg);
321
+ assert(value.len > 0);
322
+ return parse_value(T, flag, value);
323
+ }
324
+
325
+ /// Splits the value part from a `--arg=value` syntax.
326
+ fn parse_flag_split_value(flag: []const u8, arg: [:0]const u8) [:0]const u8 {
327
+ assert(flag[0] == '-' and flag[1] == '-');
328
+ assert(std.mem.startsWith(u8, arg, flag));
329
+
330
+ const value = arg[flag.len..];
331
+ if (value.len == 0) {
332
+ fatal("{s}: expected value separator '='", .{flag});
333
+ }
334
+ if (value[0] != '=') {
335
+ fatal(
336
+ "{s}: expected value separator '=', but found '{c}' in '{s}'",
337
+ .{ flag, value[0], arg },
338
+ );
339
+ }
340
+ if (value.len == 1) fatal("{s}: argument requires a value", .{flag});
341
+ return value[1..];
342
+ }
343
+
344
+ fn parse_value(comptime T: type, flag: []const u8, value: [:0]const u8) T {
345
+ assert((flag[0] == '-' and flag[1] == '-') or flag[0] == '<');
346
+ assert(value.len > 0);
347
+
348
+ const V = switch (@typeInfo(T)) {
349
+ .optional => |optional| optional.child,
350
+ else => T,
351
+ };
352
+
353
+ if (V == []const u8 or V == [:0]const u8) return value;
354
+ if (V == bool) return parse_value_bool(flag, value);
355
+ if (@typeInfo(V) == .int) return parse_value_int(V, flag, value);
356
+ if (@typeInfo(V) == .@"enum") return parse_value_enum(V, flag, value);
357
+ if (@hasDecl(V, "parse_flag_value")) {
358
+
359
+ // Contracts:
360
+ // - Input string is guaranteed to be not empty.
361
+ // - Output diagnostic must point to statically-allocated data.
362
+ // - Diagnostic must start with a lower case letter.
363
+ // - Diagnostic must end with a ':' (it will be concatenated with original input).
364
+ // - (static_diagnostic != null) iff error.InvalidFlagValue is returned.
365
+ const parse_flag_value: fn (
366
+ string: []const u8,
367
+ static_diagnostic: *?[]const u8,
368
+ ) error{InvalidFlagValue}!V = V.parse_flag_value;
369
+
370
+ var diagnostic: ?[]const u8 = null;
371
+ if (parse_flag_value(value, &diagnostic)) |result| {
372
+ assert(diagnostic == null);
373
+ return result;
374
+ } else |err| switch (err) {
375
+ error.InvalidFlagValue => {
376
+ const message = diagnostic.?;
377
+ assert(std.ascii.isLower(message[0]));
378
+ assert(message[message.len - 1] == ':');
379
+ fatal("{s}: {s} '{s}'", .{ flag, message, value });
380
+ },
381
+ }
382
+ }
383
+ comptime unreachable;
384
+ }
385
+
386
+ /// Parse string value into an integer, providing a nice error message for the user.
387
+ fn parse_value_int(comptime T: type, flag: []const u8, value: [:0]const u8) T {
388
+ assert((flag[0] == '-' and flag[1] == '-') or flag[0] == '<');
389
+
390
+ // Support only unsigned integers, as a conservative choice.
391
+ comptime assert(@typeInfo(T).int.signedness == .unsigned);
392
+ return std.fmt.parseUnsigned(T, value, 10) catch |err| {
393
+ switch (err) {
394
+ error.Overflow => fatal(
395
+ "{s}: value exceeds {d}-bit {s} integer: '{s}'",
396
+ .{ flag, @typeInfo(T).int.bits, @tagName(@typeInfo(T).int.signedness), value },
397
+ ),
398
+ error.InvalidCharacter => fatal(
399
+ "{s}: expected an integer value, but found '{s}' (invalid digit)",
400
+ .{ flag, value },
401
+ ),
402
+ }
403
+ };
404
+ }
405
+
406
+ fn parse_value_bool(flag: []const u8, value: [:0]const u8) bool {
407
+ return switch (parse_value_enum(
408
+ enum {
409
+ true,
410
+ false,
411
+ },
412
+ flag,
413
+ value,
414
+ )) {
415
+ .true => true,
416
+ .false => false,
417
+ };
418
+ }
419
+
420
+ fn parse_value_enum(comptime E: type, flag: []const u8, value: [:0]const u8) E {
421
+ assert((flag[0] == '-' and flag[1] == '-') or flag[0] == '<');
422
+ comptime assert(@typeInfo(E).@"enum".is_exhaustive);
423
+
424
+ return std.meta.stringToEnum(E, value) orelse fatal(
425
+ "{s}: expected one of {s}, but found '{s}'",
426
+ .{ flag, comptime fields_to_comma_list(E), value },
427
+ );
428
+ }
429
+
430
+ fn fields_to_comma_list(comptime E: type) []const u8 {
431
+ comptime {
432
+ const field_count = std.meta.fields(E).len;
433
+ assert(field_count >= 2);
434
+
435
+ var result: []const u8 = "";
436
+ for (std.meta.fields(E), 0..) |field, field_index| {
437
+ const separator = switch (field_index) {
438
+ 0 => "",
439
+ else => ", ",
440
+ field_count - 1 => if (field_count == 2) " or " else ", or ",
441
+ };
442
+ result = result ++ separator ++ "'" ++ field.name ++ "'";
443
+ }
444
+ return result;
445
+ }
446
+ }
447
+
448
+ fn flag_name(comptime field: std.builtin.Type.StructField) []const u8 {
449
+ return comptime blk: {
450
+ assert(!std.mem.eql(u8, field.name, "-"));
451
+ assert(!std.mem.eql(u8, field.name, "--"));
452
+
453
+ var result: []const u8 = "--";
454
+ var index = 0;
455
+ while (std.mem.indexOfScalar(u8, field.name[index..], '_')) |i| {
456
+ result = result ++ field.name[index..][0..i] ++ "-";
457
+ index = index + i + 1;
458
+ }
459
+ result = result ++ field.name[index..];
460
+ break :blk result;
461
+ };
462
+ }
463
+
464
+ test flag_name {
465
+ const field = @typeInfo(struct { statsd: bool }).@"struct".fields[0];
466
+ try std.testing.expectEqualStrings(flag_name(field), "--statsd");
467
+ }
468
+
469
+ fn flag_name_positional(comptime field: std.builtin.Type.StructField) []const u8 {
470
+ comptime assert(std.mem.indexOfScalar(u8, field.name, '_') == null);
471
+ return "<" ++ field.name ++ ">";
472
+ }
473
+
474
+ /// Fuzz parse_flag_value function:
475
+ ///
476
+ /// - Check that ok cases return a value.
477
+ /// - Check that err cases return an error with a properly formatted diagnostics.
478
+ /// - Check that the diagnostic contains specified substring
479
+ /// - Random tests with the input alphabet seeded from explicit cases.
480
+ /// - Random tests with uniform input.
481
+ pub fn parse_flag_value_fuzz(
482
+ comptime T: type,
483
+ parse_flag_value: fn ([]const u8, *?[]const u8) error{InvalidFlagValue}!T,
484
+ cases: struct {
485
+ ok: []const struct { []const u8, T },
486
+ err: []const struct { []const u8, []const u8 },
487
+ },
488
+ ) !void {
489
+ comptime assert(T.parse_flag_value == parse_flag_value);
490
+
491
+ const test_count = 50_000;
492
+ const string_size_max = 32;
493
+
494
+ const gpa = std.testing.allocator;
495
+ var prng = stdx.PRNG.from_seed_testing();
496
+
497
+ for (cases.ok) |case| {
498
+ const string, const want = case;
499
+ assert(string.len > 0);
500
+
501
+ var diagnostic: ?[]const u8 = null;
502
+ const got = try parse_flag_value(string, &diagnostic);
503
+ assert(diagnostic == null);
504
+ try std.testing.expectEqual(want, got);
505
+ }
506
+
507
+ for (cases.err) |case| {
508
+ const string, const want_message = case;
509
+ assert(string.len > 0); // Empty value are rejected early.
510
+
511
+ var diagnostic: ?[]const u8 = null;
512
+ if (parse_flag_value(string, &diagnostic)) |value| {
513
+ std.debug.print("expected an error, got value: input='{s}', value={}", .{
514
+ string,
515
+ value,
516
+ });
517
+ return error.TestUnexpectedResult;
518
+ } else |err| switch (err) {
519
+ error.InvalidFlagValue => {
520
+ try parse_flag_value_check_diagnostic(string, diagnostic);
521
+ if (stdx.cut(diagnostic.?, want_message) == null) {
522
+ std.debug.print(
523
+ "expected diagnostic to contain substring='{s}' diagnostic='{s}'",
524
+ .{ want_message, diagnostic.? },
525
+ );
526
+ return error.TestUnexpectedResult;
527
+ }
528
+ },
529
+ }
530
+ }
531
+
532
+ var corpus: std.ArrayListUnmanaged(u8) = .empty;
533
+ defer corpus.deinit(gpa);
534
+
535
+ for (cases.ok) |case| try corpus.appendSlice(gpa, case[0]);
536
+ for (cases.err) |case| try corpus.appendSlice(gpa, case[0]);
537
+ for (0..5) |_| try corpus.append(gpa, prng.int(u8));
538
+
539
+ std.mem.sort(u8, corpus.items, {}, std.sort.asc(u8));
540
+
541
+ const alphabet = unique(corpus.items);
542
+
543
+ var string_buffer: [string_size_max]u8 = @splat(0);
544
+ for (0..test_count) |_| {
545
+ const string_size = prng.range_inclusive(usize, 1, string_size_max);
546
+ const string = string_buffer[0..string_size];
547
+ assert(string.len > 0);
548
+ if (prng.boolean()) {
549
+ for (string) |*c| c.* = alphabet[prng.index(alphabet)];
550
+ } else {
551
+ for (string) |*c| c.* = prng.int(u8);
552
+ }
553
+
554
+ var diagnostic: ?[]const u8 = null;
555
+ if (parse_flag_value(string, &diagnostic)) |_| {
556
+ assert(diagnostic == null);
557
+ } else |err| switch (err) {
558
+ error.InvalidFlagValue => try parse_flag_value_check_diagnostic(string, diagnostic),
559
+ }
560
+ }
561
+ }
562
+
563
+ fn parse_flag_value_check_diagnostic(string: []const u8, diagnostic: ?[]const u8) !void {
564
+ const message = diagnostic orelse {
565
+ std.debug.print("expected a diagnostic: string='{s}'", .{string});
566
+ return error.TestUnexpectedResult;
567
+ };
568
+ if (!(message.len > 0 and
569
+ std.ascii.isLower(message[0]) and
570
+ message[message.len - 1] == ':'))
571
+ {
572
+ std.debug.print("wrong diagnostic format: string='{s}' diagnostic='{s}'", .{
573
+ string,
574
+ message,
575
+ });
576
+ return error.TestUnexpectedResult;
577
+ }
578
+ }
579
+
580
+ fn unique(sorted: []u8) []u8 {
581
+ assert(sorted.len > 0);
582
+
583
+ var count: usize = 1;
584
+ for (1..sorted.len) |index| {
585
+ assert(sorted[count - 1] <= sorted[index]);
586
+ if (sorted[count - 1] == sorted[index]) {
587
+ // Duplicate! Skip to the next index.
588
+ } else {
589
+ sorted[count] = sorted[index];
590
+ count += 1;
591
+ }
592
+ }
593
+
594
+ return sorted[0..count];
595
+ }
596
+
597
+ // CLI parsing makes a liberal use of `fatal`, so testing it within the process is impossible. We
598
+ // test it out of process by:
599
+ // - using Zig compiler to build this very file as an executable in a temporary directory,
600
+ // - running the following main with various args and capturing stdout, stderr, and the exit code.
601
+ // - asserting that the captured values are correct.
602
+ // For production builds, don't include the main function.
603
+ // This is `if __name__ == "__main__":` at comptime!
604
+ pub const main =
605
+ if (@import("root") != @This()) {} else struct {
606
+ const CLIArgs = union(enum) {
607
+ empty,
608
+ prefix: struct {
609
+ foo: u8 = 0,
610
+ foo_bar: u8 = 0,
611
+ opt: bool = false,
612
+ option: bool = false,
613
+ },
614
+ positional: struct {
615
+ flag: bool = false,
616
+
617
+ @"--": void,
618
+ p1: []const u8,
619
+ p2: []const u8,
620
+ p3: ?u32 = null,
621
+ p4: ?u32 = null,
622
+ },
623
+ extended: struct {
624
+ flag: bool = false,
625
+ @"--": void,
626
+ },
627
+ required: struct {
628
+ foo: u8,
629
+ bar: u8,
630
+ },
631
+ values: struct {
632
+ int: u32 = 0,
633
+ size: stdx.ByteSize = .{ .value = 0 },
634
+ boolean: bool = false,
635
+ path: []const u8 = "not-set",
636
+ optional: ?[]const u8 = null,
637
+ choice: enum { marlowe, shakespeare } = .marlowe,
638
+ },
639
+ subcommand: union(enum) {
640
+ pub const help =
641
+ \\subcommand help
642
+ \\
643
+ ;
644
+
645
+ c1: struct { a: bool = false },
646
+ c2: struct { b: bool = false },
647
+ },
648
+
649
+ pub const help =
650
+ \\ flags-test-program [flags]
651
+ \\
652
+ ;
653
+ };
654
+
655
+ fn main() !void {
656
+ var gpa_allocator = std.heap.GeneralPurposeAllocator(.{}){};
657
+ const gpa = gpa_allocator.allocator();
658
+
659
+ var args = try std.process.argsWithAllocator(gpa);
660
+ defer args.deinit();
661
+
662
+ const cli_args = parse(&args, CLIArgs);
663
+
664
+ const stdout = std.io.getStdOut();
665
+ const out_stream = stdout.writer();
666
+ switch (cli_args) {
667
+ .empty => try out_stream.print("empty\n", .{}),
668
+ .prefix => |values| {
669
+ try out_stream.print("foo: {}\n", .{values.foo});
670
+ try out_stream.print("foo-bar: {}\n", .{values.foo_bar});
671
+ try out_stream.print("opt: {}\n", .{values.opt});
672
+ try out_stream.print("option: {}\n", .{values.option});
673
+ },
674
+ .positional => |values| {
675
+ try out_stream.print("p1: {s}\n", .{values.p1});
676
+ try out_stream.print("p2: {s}\n", .{values.p2});
677
+ try out_stream.print("p3: {?}\n", .{values.p3});
678
+ try out_stream.print("p4: {?}\n", .{values.p4});
679
+ try out_stream.print("flag: {}\n", .{values.flag});
680
+ },
681
+ .extended => |values| {
682
+ try out_stream.print("flag: {}\n", .{values.flag});
683
+ while (args.next()) |arg| try out_stream.print("arg: {s}\n", .{arg});
684
+ },
685
+ .required => |required| {
686
+ try out_stream.print("foo: {}\n", .{required.foo});
687
+ try out_stream.print("bar: {}\n", .{required.bar});
688
+ },
689
+ .values => |values| {
690
+ try out_stream.print("int: {}\n", .{values.int});
691
+ try out_stream.print("size: {}\n", .{values.size.bytes()});
692
+ try out_stream.print("boolean: {}\n", .{values.boolean});
693
+ try out_stream.print("path: {s}\n", .{values.path});
694
+ try out_stream.print("optional: {?s}\n", .{values.optional});
695
+ try out_stream.print("choice: {?s}\n", .{@tagName(values.choice)});
696
+ },
697
+ .subcommand => |values| {
698
+ switch (values) {
699
+ .c1 => |c1| try out_stream.print("c1.a: {}\n", .{c1.a}),
700
+ .c2 => |c2| try out_stream.print("c2.b: {}\n", .{c2.b}),
701
+ }
702
+ },
703
+ }
704
+ }
705
+ }.main;
706
+
707
+ test "flags" {
708
+ const Snap = stdx.Snap;
709
+ const module_path = "src/stdx";
710
+ const snap = Snap.snap_fn(module_path);
711
+
712
+ const T = struct {
713
+ const T = @This();
714
+
715
+ gpa: std.mem.Allocator,
716
+ tmp_dir: std.testing.TmpDir,
717
+ output_buf: std.ArrayList(u8),
718
+ flags_exe_buf: *[std.fs.max_path_bytes]u8,
719
+ flags_exe: []const u8,
720
+
721
+ fn init(gpa: std.mem.Allocator) !T {
722
+ // TODO: Avoid std.posix.getenv() as it currently causes a linker error on windows.
723
+ // See: https://github.com/ziglang/zig/issues/8456
724
+ const zig_exe = try std.process.getEnvVarOwned(gpa, "ZIG_EXE"); // Set by build.zig
725
+ defer gpa.free(zig_exe);
726
+
727
+ var tmp_dir = std.testing.tmpDir(.{});
728
+ errdefer tmp_dir.cleanup();
729
+
730
+ const tmp_dir_path = try std.fs.path.join(gpa, &.{
731
+ ".zig-cache",
732
+ "tmp",
733
+ &tmp_dir.sub_path,
734
+ });
735
+ defer gpa.free(tmp_dir_path);
736
+
737
+ const output_buf = std.ArrayList(u8).init(gpa);
738
+ errdefer output_buf.deinit();
739
+
740
+ const flags_exe_buf = try gpa.create([std.fs.max_path_bytes]u8);
741
+ errdefer gpa.destroy(flags_exe_buf);
742
+
743
+ { // Compile this file as an executable!
744
+ const path_relative = try std.fs.path.join(gpa, &.{
745
+ module_path,
746
+ @src().file,
747
+ });
748
+ defer gpa.free(path_relative);
749
+
750
+ const this_file = try std.fs.cwd().realpath(
751
+ path_relative,
752
+ flags_exe_buf,
753
+ );
754
+ const argv = [_][]const u8{ zig_exe, "build-exe", this_file };
755
+ const exec_result = try std.process.Child.run(.{
756
+ .allocator = gpa,
757
+ .argv = &argv,
758
+ .cwd = tmp_dir_path,
759
+ });
760
+ defer gpa.free(exec_result.stdout);
761
+ defer gpa.free(exec_result.stderr);
762
+
763
+ if (exec_result.term.Exited != 0) {
764
+ std.debug.print("{s}{s}", .{ exec_result.stdout, exec_result.stderr });
765
+ return error.FailedToCompile;
766
+ }
767
+ }
768
+
769
+ const flags_exe = try tmp_dir.dir.realpath(
770
+ "flags" ++ comptime builtin.target.exeFileExt(),
771
+ flags_exe_buf,
772
+ );
773
+
774
+ const sanity_check = try std.fs.openFileAbsolute(flags_exe, .{});
775
+ sanity_check.close();
776
+
777
+ return .{
778
+ .gpa = gpa,
779
+ .tmp_dir = tmp_dir,
780
+ .output_buf = output_buf,
781
+ .flags_exe_buf = flags_exe_buf,
782
+ .flags_exe = flags_exe,
783
+ };
784
+ }
785
+
786
+ fn deinit(t: *T) void {
787
+ t.gpa.destroy(t.flags_exe_buf);
788
+ t.output_buf.deinit();
789
+ t.tmp_dir.cleanup();
790
+ t.* = undefined;
791
+ }
792
+
793
+ fn check(t: *T, cli: []const []const u8, want: Snap) !void {
794
+ const argv = try t.gpa.alloc([]const u8, cli.len + 1);
795
+ defer t.gpa.free(argv);
796
+
797
+ argv[0] = t.flags_exe;
798
+ for (argv[1..], 0..) |*arg, i| {
799
+ arg.* = cli[i];
800
+ }
801
+ if (cli.len > 0) {
802
+ assert(argv[argv.len - 1].ptr == cli[cli.len - 1].ptr);
803
+ }
804
+
805
+ const exec_result = try std.process.Child.run(.{
806
+ .allocator = t.gpa,
807
+ .argv = argv,
808
+ });
809
+ defer t.gpa.free(exec_result.stdout);
810
+ defer t.gpa.free(exec_result.stderr);
811
+
812
+ t.output_buf.clearRetainingCapacity();
813
+
814
+ if (exec_result.term.Exited != 0) {
815
+ try t.output_buf.writer().print("status: {}\n", .{exec_result.term.Exited});
816
+ }
817
+ if (exec_result.stdout.len > 0) {
818
+ try t.output_buf.writer().print("stdout:\n{s}", .{exec_result.stdout});
819
+ }
820
+ if (exec_result.stderr.len > 0) {
821
+ try t.output_buf.writer().print("stderr:\n{s}", .{exec_result.stderr});
822
+ }
823
+
824
+ try want.diff(t.output_buf.items);
825
+ }
826
+ };
827
+
828
+ var t = try T.init(std.testing.allocator);
829
+ defer t.deinit();
830
+
831
+ // Test-cases are roughly in the source order of the corresponding features.
832
+
833
+ try t.check(&.{"empty"}, snap(@src(),
834
+ \\stdout:
835
+ \\empty
836
+ \\
837
+ ));
838
+
839
+ try t.check(&.{}, snap(@src(),
840
+ \\status: 1
841
+ \\stderr:
842
+ \\error: subcommand required, expected 'empty', 'prefix', 'positional', 'extended', 'required', 'values', or 'subcommand'
843
+ \\
844
+ ));
845
+
846
+ try t.check(&.{"-h"}, snap(@src(),
847
+ \\stdout:
848
+ \\ flags-test-program [flags]
849
+ \\
850
+ ));
851
+
852
+ try t.check(&.{"--help"}, snap(@src(),
853
+ \\stdout:
854
+ \\ flags-test-program [flags]
855
+ \\
856
+ ));
857
+
858
+ try t.check(&.{""}, snap(@src(),
859
+ \\status: 1
860
+ \\stderr:
861
+ \\error: unknown subcommand: ''
862
+ \\
863
+ ));
864
+
865
+ try t.check(&.{"bogus"}, snap(@src(),
866
+ \\status: 1
867
+ \\stderr:
868
+ \\error: unknown subcommand: 'bogus'
869
+ \\
870
+ ));
871
+
872
+ try t.check(&.{"--int=92"}, snap(@src(),
873
+ \\status: 1
874
+ \\stderr:
875
+ \\error: unknown subcommand: '--int=92'
876
+ \\
877
+ ));
878
+
879
+ try t.check(&.{ "empty", "--help" }, snap(@src(),
880
+ \\status: 1
881
+ \\stderr:
882
+ \\error: unexpected argument: '--help'
883
+ \\
884
+ ));
885
+
886
+ try t.check(&.{ "prefix", "--foo=92" }, snap(@src(),
887
+ \\stdout:
888
+ \\foo: 92
889
+ \\foo-bar: 0
890
+ \\opt: false
891
+ \\option: false
892
+ \\
893
+ ));
894
+
895
+ try t.check(&.{ "prefix", "--foo-bar=92" }, snap(@src(),
896
+ \\stdout:
897
+ \\foo: 0
898
+ \\foo-bar: 92
899
+ \\opt: false
900
+ \\option: false
901
+ \\
902
+ ));
903
+
904
+ try t.check(&.{ "prefix", "--foo-baz=92" }, snap(@src(),
905
+ \\status: 1
906
+ \\stderr:
907
+ \\error: --foo: expected value separator '=', but found '-' in '--foo-baz=92'
908
+ \\
909
+ ));
910
+
911
+ try t.check(&.{ "prefix", "--opt" }, snap(@src(),
912
+ \\stdout:
913
+ \\foo: 0
914
+ \\foo-bar: 0
915
+ \\opt: true
916
+ \\option: false
917
+ \\
918
+ ));
919
+
920
+ try t.check(&.{ "prefix", "--option" }, snap(@src(),
921
+ \\stdout:
922
+ \\foo: 0
923
+ \\foo-bar: 0
924
+ \\opt: false
925
+ \\option: true
926
+ \\
927
+ ));
928
+
929
+ try t.check(&.{ "prefix", "--optx" }, snap(@src(),
930
+ \\status: 1
931
+ \\stderr:
932
+ \\error: --opt: expected value separator '=', but found 'x' in '--optx'
933
+ \\
934
+ ));
935
+
936
+ try t.check(&.{ "positional", "x", "y" }, snap(@src(),
937
+ \\stdout:
938
+ \\p1: x
939
+ \\p2: y
940
+ \\p3: null
941
+ \\p4: null
942
+ \\flag: false
943
+ \\
944
+ ));
945
+
946
+ try t.check(&.{ "positional", "x", "y", "1" }, snap(@src(),
947
+ \\stdout:
948
+ \\p1: x
949
+ \\p2: y
950
+ \\p3: 1
951
+ \\p4: null
952
+ \\flag: false
953
+ \\
954
+ ));
955
+
956
+ try t.check(&.{ "positional", "x", "y", "1", "2" }, snap(@src(),
957
+ \\stdout:
958
+ \\p1: x
959
+ \\p2: y
960
+ \\p3: 1
961
+ \\p4: 2
962
+ \\flag: false
963
+ \\
964
+ ));
965
+
966
+ try t.check(&.{"positional"}, snap(@src(),
967
+ \\status: 1
968
+ \\stderr:
969
+ \\error: <p1>: argument is required
970
+ \\
971
+ ));
972
+
973
+ try t.check(&.{ "positional", "x" }, snap(@src(),
974
+ \\status: 1
975
+ \\stderr:
976
+ \\error: <p2>: argument is required
977
+ \\
978
+ ));
979
+
980
+ try t.check(&.{ "positional", "x", "y", "z" }, snap(@src(),
981
+ \\status: 1
982
+ \\stderr:
983
+ \\error: <p3>: expected an integer value, but found 'z' (invalid digit)
984
+ \\
985
+ ));
986
+
987
+ try t.check(&.{ "positional", "x", "y", "1", "2", "3" }, snap(@src(),
988
+ \\status: 1
989
+ \\stderr:
990
+ \\error: unexpected argument: '3'
991
+ \\
992
+ ));
993
+
994
+ try t.check(&.{ "positional", "" }, snap(@src(),
995
+ \\status: 1
996
+ \\stderr:
997
+ \\error: <p1>: empty argument
998
+ \\
999
+ ));
1000
+
1001
+ try t.check(&.{ "positional", "x", "--flag" }, snap(@src(),
1002
+ \\status: 1
1003
+ \\stderr:
1004
+ \\error: unexpected trailing option: '--flag'
1005
+ \\
1006
+ ));
1007
+
1008
+ try t.check(&.{ "positional", "x", "--flag", "y" }, snap(@src(),
1009
+ \\status: 1
1010
+ \\stderr:
1011
+ \\error: unexpected trailing option: '--flag'
1012
+ \\
1013
+ ));
1014
+
1015
+ try t.check(&.{ "positional", "--flag", "x", "y" }, snap(@src(),
1016
+ \\stdout:
1017
+ \\p1: x
1018
+ \\p2: y
1019
+ \\p3: null
1020
+ \\p4: null
1021
+ \\flag: true
1022
+ \\
1023
+ ));
1024
+
1025
+ try t.check(&.{ "positional", "--", "x", "y" }, snap(@src(),
1026
+ \\status: 1
1027
+ \\stderr:
1028
+ \\error: unexpected argument: '--'
1029
+ \\
1030
+ ));
1031
+
1032
+ try t.check(&.{ "positional", "--flak", "x", "y" }, snap(@src(),
1033
+ \\status: 1
1034
+ \\stderr:
1035
+ \\error: unexpected argument: '--flak'
1036
+ \\
1037
+ ));
1038
+
1039
+ try t.check(&.{ "required", "--foo=1", "--bar=2" }, snap(@src(),
1040
+ \\stdout:
1041
+ \\foo: 1
1042
+ \\bar: 2
1043
+ \\
1044
+ ));
1045
+
1046
+ try t.check(&.{ "required", "--surprise" }, snap(@src(),
1047
+ \\status: 1
1048
+ \\stderr:
1049
+ \\error: unexpected argument: '--surprise'
1050
+ \\
1051
+ ));
1052
+
1053
+ try t.check(&.{ "required", "--foo=1" }, snap(@src(),
1054
+ \\status: 1
1055
+ \\stderr:
1056
+ \\error: --bar: argument is required
1057
+ \\
1058
+ ));
1059
+
1060
+ try t.check(&.{ "required", "--foo=1", "--bar=2", "--foo=3" }, snap(@src(),
1061
+ \\status: 1
1062
+ \\stderr:
1063
+ \\error: --foo: duplicate argument
1064
+ \\
1065
+ ));
1066
+
1067
+ try t.check(&.{
1068
+ "values",
1069
+ "--int=92",
1070
+ "--size=1GiB",
1071
+ "--boolean",
1072
+ "--path=/home",
1073
+ "--optional=some",
1074
+ "--choice=shakespeare",
1075
+ }, snap(@src(),
1076
+ \\stdout:
1077
+ \\int: 92
1078
+ \\size: 1073741824
1079
+ \\boolean: true
1080
+ \\path: /home
1081
+ \\optional: some
1082
+ \\choice: shakespeare
1083
+ \\
1084
+ ));
1085
+
1086
+ try t.check(&.{"values"}, snap(@src(),
1087
+ \\stdout:
1088
+ \\int: 0
1089
+ \\size: 0
1090
+ \\boolean: false
1091
+ \\path: not-set
1092
+ \\optional: null
1093
+ \\choice: marlowe
1094
+ \\
1095
+ ));
1096
+
1097
+ try t.check(&.{ "values", "--boolean=true" }, snap(@src(),
1098
+ \\stdout:
1099
+ \\int: 0
1100
+ \\size: 0
1101
+ \\boolean: true
1102
+ \\path: not-set
1103
+ \\optional: null
1104
+ \\choice: marlowe
1105
+ \\
1106
+ ));
1107
+
1108
+ try t.check(&.{ "values", "--boolean=false" }, snap(@src(),
1109
+ \\stdout:
1110
+ \\int: 0
1111
+ \\size: 0
1112
+ \\boolean: false
1113
+ \\path: not-set
1114
+ \\optional: null
1115
+ \\choice: marlowe
1116
+ \\
1117
+ ));
1118
+
1119
+ try t.check(&.{ "values", "--boolean=foo" }, snap(@src(),
1120
+ \\status: 1
1121
+ \\stderr:
1122
+ \\error: --boolean: expected one of 'true' or 'false', but found 'foo'
1123
+ \\
1124
+ ));
1125
+
1126
+ try t.check(&.{ "values", "--int" }, snap(@src(),
1127
+ \\status: 1
1128
+ \\stderr:
1129
+ \\error: --int: expected value separator '='
1130
+ \\
1131
+ ));
1132
+
1133
+ try t.check(&.{ "values", "--int:" }, snap(@src(),
1134
+ \\status: 1
1135
+ \\stderr:
1136
+ \\error: --int: expected value separator '=', but found ':' in '--int:'
1137
+ \\
1138
+ ));
1139
+
1140
+ try t.check(&.{ "values", "--int=" }, snap(@src(),
1141
+ \\status: 1
1142
+ \\stderr:
1143
+ \\error: --int: argument requires a value
1144
+ \\
1145
+ ));
1146
+
1147
+ try t.check(&.{ "values", "--int=-92" }, snap(@src(),
1148
+ \\status: 1
1149
+ \\stderr:
1150
+ \\error: --int: expected an integer value, but found '-92' (invalid digit)
1151
+ \\
1152
+ ));
1153
+
1154
+ try t.check(&.{ "values", "--int=_92" }, snap(@src(),
1155
+ \\status: 1
1156
+ \\stderr:
1157
+ \\error: --int: expected an integer value, but found '_92' (invalid digit)
1158
+ \\
1159
+ ));
1160
+
1161
+ try t.check(&.{ "values", "--int=92_" }, snap(@src(),
1162
+ \\status: 1
1163
+ \\stderr:
1164
+ \\error: --int: expected an integer value, but found '92_' (invalid digit)
1165
+ \\
1166
+ ));
1167
+
1168
+ try t.check(&.{ "values", "--int=92" }, snap(@src(),
1169
+ \\stdout:
1170
+ \\int: 92
1171
+ \\size: 0
1172
+ \\boolean: false
1173
+ \\path: not-set
1174
+ \\optional: null
1175
+ \\choice: marlowe
1176
+ \\
1177
+ ));
1178
+
1179
+ try t.check(&.{ "values", "--int=900_200" }, snap(@src(),
1180
+ \\stdout:
1181
+ \\int: 900200
1182
+ \\size: 0
1183
+ \\boolean: false
1184
+ \\path: not-set
1185
+ \\optional: null
1186
+ \\choice: marlowe
1187
+ \\
1188
+ ));
1189
+
1190
+ try t.check(&.{ "values", "--int=XCII" }, snap(@src(),
1191
+ \\status: 1
1192
+ \\stderr:
1193
+ \\error: --int: expected an integer value, but found 'XCII' (invalid digit)
1194
+ \\
1195
+ ));
1196
+
1197
+ try t.check(&.{ "values", "--int=44444444444444444444" }, snap(@src(),
1198
+ \\status: 1
1199
+ \\stderr:
1200
+ \\error: --int: value exceeds 32-bit unsigned integer: '44444444444444444444'
1201
+ \\
1202
+ ));
1203
+
1204
+ try t.check(&.{ "values", "--int=-0" }, snap(@src(),
1205
+ \\status: 1
1206
+ \\stderr:
1207
+ \\error: --int: expected an integer value, but found '-0' (invalid digit)
1208
+ \\
1209
+ ));
1210
+
1211
+ try t.check(&.{ "values", "--int=+0" }, snap(@src(),
1212
+ \\status: 1
1213
+ \\stderr:
1214
+ \\error: --int: expected an integer value, but found '+0' (invalid digit)
1215
+ \\
1216
+ ));
1217
+
1218
+ try t.check(&.{ "values", "--size=-0" }, snap(@src(),
1219
+ \\status: 1
1220
+ \\stderr:
1221
+ \\error: --size: expected a size, but found: '-0'
1222
+ \\
1223
+ ));
1224
+
1225
+ try t.check(&.{ "values", "--size=+0" }, snap(@src(),
1226
+ \\status: 1
1227
+ \\stderr:
1228
+ \\error: --size: expected a size, but found: '+0'
1229
+ \\
1230
+ ));
1231
+
1232
+ try t.check(&.{ "values", "--size=_1000KiB" }, snap(@src(),
1233
+ \\status: 1
1234
+ \\stderr:
1235
+ \\error: --size: expected a size, but found: '_1000KiB'
1236
+ \\
1237
+ ));
1238
+
1239
+ try t.check(&.{ "values", "--size=1000_KiB" }, snap(@src(),
1240
+ \\status: 1
1241
+ \\stderr:
1242
+ \\error: --size: expected a size, but found: '1000_KiB'
1243
+ \\
1244
+ ));
1245
+
1246
+ try t.check(&.{ "values", "--size=1_000KiB" }, snap(@src(),
1247
+ \\stdout:
1248
+ \\int: 0
1249
+ \\size: 1024000
1250
+ \\boolean: false
1251
+ \\path: not-set
1252
+ \\optional: null
1253
+ \\choice: marlowe
1254
+ \\
1255
+ ));
1256
+
1257
+ try t.check(&.{ "values", "--size=3MiB" }, snap(@src(),
1258
+ \\stdout:
1259
+ \\int: 0
1260
+ \\size: 3145728
1261
+ \\boolean: false
1262
+ \\path: not-set
1263
+ \\optional: null
1264
+ \\choice: marlowe
1265
+ \\
1266
+ ));
1267
+
1268
+ try t.check(&.{ "values", "--size=44444444444444444444" }, snap(@src(),
1269
+ \\status: 1
1270
+ \\stderr:
1271
+ \\error: --size: value exceeds 64-bit unsigned integer: '44444444444444444444'
1272
+ \\
1273
+ ));
1274
+
1275
+ try t.check(&.{ "values", "--size=100000000000000000" }, snap(@src(),
1276
+ \\stdout:
1277
+ \\int: 0
1278
+ \\size: 100000000000000000
1279
+ \\boolean: false
1280
+ \\path: not-set
1281
+ \\optional: null
1282
+ \\choice: marlowe
1283
+ \\
1284
+ ));
1285
+
1286
+ try t.check(&.{ "values", "--size=100000000000000000kib" }, snap(@src(),
1287
+ \\status: 1
1288
+ \\stderr:
1289
+ \\error: --size: size in bytes exceeds 64-bit unsigned integer: '100000000000000000kib'
1290
+ \\
1291
+ ));
1292
+
1293
+ try t.check(&.{ "values", "--size=3bogus" }, snap(@src(),
1294
+ \\status: 1
1295
+ \\stderr:
1296
+ \\error: --size: invalid unit in size, needed KiB, MiB, GiB or TiB: '3bogus'
1297
+ \\
1298
+ ));
1299
+
1300
+ try t.check(&.{ "values", "--size=MiB" }, snap(@src(),
1301
+ \\status: 1
1302
+ \\stderr:
1303
+ \\error: --size: expected a size, but found: 'MiB'
1304
+ \\
1305
+ ));
1306
+
1307
+ try t.check(&.{ "values", "--path=" }, snap(@src(),
1308
+ \\status: 1
1309
+ \\stderr:
1310
+ \\error: --path: argument requires a value
1311
+ \\
1312
+ ));
1313
+
1314
+ try t.check(&.{ "values", "--optional=" }, snap(@src(),
1315
+ \\status: 1
1316
+ \\stderr:
1317
+ \\error: --optional: argument requires a value
1318
+ \\
1319
+ ));
1320
+
1321
+ try t.check(&.{ "values", "--choice=molière" }, snap(@src(),
1322
+ \\status: 1
1323
+ \\stderr:
1324
+ \\error: --choice: expected one of 'marlowe' or 'shakespeare', but found 'molière'
1325
+ \\
1326
+ ));
1327
+
1328
+ try t.check(&.{"subcommand"}, snap(@src(),
1329
+ \\status: 1
1330
+ \\stderr:
1331
+ \\error: subcommand required, expected 'c1' or 'c2'
1332
+ \\
1333
+ ));
1334
+ try t.check(&.{ "subcommand", "c1", "--a" }, snap(@src(),
1335
+ \\stdout:
1336
+ \\c1.a: true
1337
+ \\
1338
+ ));
1339
+ try t.check(&.{ "subcommand", "c2", "--b" }, snap(@src(),
1340
+ \\stdout:
1341
+ \\c2.b: true
1342
+ \\
1343
+ ));
1344
+ try t.check(&.{ "subcommand", "c1", "--b" }, snap(@src(),
1345
+ \\status: 1
1346
+ \\stderr:
1347
+ \\error: unexpected argument: '--b'
1348
+ \\
1349
+ ));
1350
+ try t.check(&.{ "subcommand", "c2", "--a" }, snap(@src(),
1351
+ \\status: 1
1352
+ \\stderr:
1353
+ \\error: unexpected argument: '--a'
1354
+ \\
1355
+ ));
1356
+ try t.check(&.{ "subcommand", "--help" }, snap(@src(),
1357
+ \\stdout:
1358
+ \\subcommand help
1359
+ \\
1360
+ ));
1361
+ try t.check(&.{ "subcommand", "-h" }, snap(@src(),
1362
+ \\stdout:
1363
+ \\subcommand help
1364
+ \\
1365
+ ));
1366
+
1367
+ try t.check(&.{"extended"}, snap(@src(),
1368
+ \\stdout:
1369
+ \\flag: false
1370
+ \\
1371
+ ));
1372
+ try t.check(&.{ "extended", "--" }, snap(@src(),
1373
+ \\stdout:
1374
+ \\flag: false
1375
+ \\
1376
+ ));
1377
+ try t.check(&.{ "extended", "--flag", "--" }, snap(@src(),
1378
+ \\stdout:
1379
+ \\flag: true
1380
+ \\
1381
+ ));
1382
+ try t.check(&.{ "extended", "a" }, snap(@src(),
1383
+ \\status: 1
1384
+ \\stderr:
1385
+ \\error: unexpected argument: 'a'; expected '-- ...'
1386
+ \\
1387
+ ));
1388
+ try t.check(&.{ "extended", "--", "a" }, snap(@src(),
1389
+ \\stdout:
1390
+ \\flag: false
1391
+ \\arg: a
1392
+ \\
1393
+ ));
1394
+ try t.check(&.{ "extended", "--flag", "--", "a" }, snap(@src(),
1395
+ \\stdout:
1396
+ \\flag: true
1397
+ \\arg: a
1398
+ \\
1399
+ ));
1400
+ try t.check(&.{ "extended", "--", "a", "b" }, snap(@src(),
1401
+ \\stdout:
1402
+ \\flag: false
1403
+ \\arg: a
1404
+ \\arg: b
1405
+ \\
1406
+ ));
1407
+ try t.check(&.{ "extended", "--flag", "--", "a", "b" }, snap(@src(),
1408
+ \\stdout:
1409
+ \\flag: true
1410
+ \\arg: a
1411
+ \\arg: b
1412
+ \\
1413
+ ));
1414
+ }