├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── deploy-doc.yml │ ├── edit_dune_project_dot_ml │ ├── more-ci.yml │ └── test-deploy-doc.yml ├── .gitignore ├── .ocamlformat ├── .vscode └── settings.json ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── cmdlang-stdlib-runner.opam ├── cmdlang-stdlib-runner.opam.template ├── cmdlang-tests.opam ├── cmdlang-tests.opam.template ├── cmdlang-to-base.opam ├── cmdlang-to-base.opam.template ├── cmdlang-to-climate.opam ├── cmdlang-to-climate.opam.template ├── cmdlang-to-cmdliner.opam ├── cmdlang-to-cmdliner.opam.template ├── cmdlang.opam ├── cmdlang.opam.template ├── doc ├── .gitignore ├── babel.config.js ├── blog │ ├── 2024-08-07-hello │ │ └── index.md │ ├── 2024-09-07-yet-another-cli │ │ └── index.md │ ├── 2024-11-15-first-release │ │ └── index.md │ ├── authors.yml │ └── tags.yml ├── docs │ ├── explanation │ │ ├── README.md │ │ ├── faq.md │ │ └── future_plans.md │ ├── guides │ │ └── usage-styles │ │ │ ├── README.md │ │ │ ├── dune │ │ │ ├── lib │ │ │ ├── dune │ │ │ ├── usage_styles.ml │ │ │ └── usage_styles.mli │ │ │ └── prelude.txt │ ├── reference │ │ ├── dune │ │ ├── odoc.md │ │ └── prelude.txt │ └── tutorials │ │ └── getting-started │ │ ├── README.md │ │ ├── bin │ │ ├── dune │ │ └── main.ml │ │ ├── dune │ │ ├── lib │ │ ├── dune │ │ ├── getting_started.ml │ │ └── getting_started.mli │ │ └── prelude.txt ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── css │ │ └── custom.css │ └── pages │ │ └── index.md ├── static │ ├── .nojekyll │ ├── img │ │ ├── cmdlang.jpg │ │ └── favicon.ico │ └── odoc │ │ └── index.html └── tsconfig.json ├── dune ├── dune-project ├── lib ├── cmdlang │ ├── src │ │ ├── command.ml │ │ ├── command.mli │ │ └── dune │ └── test │ │ ├── dune │ │ ├── test__command.ml │ │ └── test__command.mli ├── cmdlang_ast │ ├── src │ │ ├── ast.ml │ │ ├── ast.mli │ │ └── dune │ └── test │ │ └── dune ├── cmdlang_stdlib_runner │ ├── src │ │ ├── arg_runner.ml │ │ ├── arg_runner.mli │ │ ├── arg_state.ml │ │ ├── arg_state.mli │ │ ├── cmdlang_stdlib_runner.ml │ │ ├── cmdlang_stdlib_runner.mli │ │ ├── command_selector.ml │ │ ├── command_selector.mli │ │ ├── dune │ │ ├── import.ml │ │ ├── import.mli │ │ ├── param_parser.ml │ │ ├── param_parser.mli │ │ ├── parser_state.ml │ │ ├── parser_state.mli │ │ ├── positional_state.ml │ │ └── positional_state.mli │ └── test │ │ ├── dune │ │ ├── test__param_parser.ml │ │ ├── test__param_parser.mli │ │ ├── test__positional_state.ml │ │ ├── test__positional_state.mli │ │ ├── test__stdlib_runner.ml │ │ └── test__stdlib_runner.mli ├── cmdlang_to_base │ ├── src │ │ ├── dune │ │ ├── translate.ml │ │ └── translate.mli │ └── test │ │ ├── cram │ │ ├── bin │ │ │ ├── dune │ │ │ ├── main_base.ml │ │ │ └── main_climate.ml │ │ ├── dune │ │ ├── run.t │ │ └── src │ │ │ ├── cmd.ml │ │ │ ├── cmd.mli │ │ │ └── dune │ │ ├── dune │ │ ├── test__param.ml │ │ └── test__param.mli ├── cmdlang_to_climate │ ├── src │ │ ├── dune │ │ ├── translate.ml │ │ └── translate.mli │ └── test │ │ ├── dune │ │ ├── test__param.ml │ │ └── test__param.mli └── cmdlang_to_cmdliner │ ├── src │ ├── dune │ ├── translate.ml │ └── translate.mli │ └── test │ ├── dune │ ├── test__manpage_of_readme.ml │ ├── test__manpage_of_readme.mli │ ├── test__param.ml │ └── test__param.mli └── test ├── cram ├── README.md ├── basic.t ├── bin │ ├── base │ │ ├── dune │ │ └── main_base.ml │ ├── climate │ │ ├── dune │ │ └── main_climate.ml │ ├── cmdliner │ │ ├── dune │ │ └── main_cmdliner.ml │ └── stdlib-runner │ │ ├── dune │ │ └── main_stdlib_runner.ml ├── const.t ├── doc.t ├── dune ├── enum.t ├── flags.t ├── group.t ├── main-help.t ├── named-opt.t ├── named-with-default.t └── src │ ├── cmd.ml │ ├── cmd.mli │ └── dune └── expect ├── README.md ├── arg_test.ml ├── arg_test.mli ├── climate_non_ret.ml ├── climate_non_ret.mli ├── dune ├── test__applicative_operations.ml ├── test__applicative_operations.mli ├── test__cmd_name_with_underscore.ml ├── test__cmd_name_with_underscore.mli ├── test__flag.ml ├── test__flag.mli ├── test__help.ml ├── test__help.mli ├── test__invalid_pos_opt.ml ├── test__invalid_pos_opt.mli ├── test__named.ml ├── test__named.mli ├── test__negative_int_args.ml ├── test__negative_int_args.mli ├── test__param.ml ├── test__param.mli ├── test__pos.ml └── test__pos.mli /.gitattributes: -------------------------------------------------------------------------------- 1 | # Tell github that .ml and .mli files are OCaml 2 | *.ml linguist-language=OCaml 3 | *.mli linguist-language=OCaml 4 | 5 | # Disable syntax detection for cram tests 6 | *.t linguist-language=Text 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "**" # This will match pull requests targeting any branch 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | ocaml-compiler: 19 | - 5.3.x 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup OCaml 28 | uses: ocaml/setup-ocaml@v3 29 | with: 30 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 31 | opam-repositories: | 32 | default: https://github.com/ocaml/opam-repository.git 33 | mbarbin: https://github.com/mbarbin/opam-repository.git 34 | # janestreet-bleeding: https://github.com/janestreet/opam-repository.git 35 | # janestreet-bleeding-external: https://github.com/janestreet/opam-repository.git#external-packages 36 | 37 | # Setting `(implicit_transitive_deps VALUE)` conditionally based on the compiler version. 38 | - name: Edit dune-project 39 | run: opam exec -- ocaml .github/workflows/edit_dune_project_dot_ml "${{ matrix.ocaml-compiler }}" 40 | 41 | - name: Install dependencies 42 | run: opam install . --deps-only --with-doc --with-test --with-dev-setup 43 | 44 | - name: Build 45 | run: opam exec -- dune build @all @lint 46 | 47 | - name: Run tests 48 | run: | 49 | mkdir $BISECT_DIR 50 | opam exec -- dune runtest --instrument-with bisect_ppx 51 | env: 52 | BISECT_DIR: ${{ runner.temp }}/_bisect_ppx_data 53 | BISECT_FILE: ${{ runner.temp }}/_bisect_ppx_data/data 54 | 55 | - name: Send coverage report to Coveralls 56 | run: opam exec -- bisect-ppx-report send-to Coveralls --coverage-path $BISECT_DIR 57 | env: 58 | BISECT_DIR: ${{ runner.temp }}/_bisect_ppx_data 59 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | PULL_REQUEST_NUMBER: ${{ github.event.number }} 61 | 62 | # Before checking for uncommitted changes we need to restore changes 63 | # potentially made to the dune-project file. 64 | - name: Restore dune-project 65 | run: git restore dune-project 66 | 67 | - name: Check for uncommitted changes 68 | run: git diff --exit-code 69 | 70 | - name: Lint opam 71 | uses: ocaml/setup-ocaml/lint-opam@v3 72 | 73 | - name: Lint fmt 74 | uses: ocaml/setup-ocaml/lint-fmt@v3 75 | 76 | - name: Lint doc 77 | uses: ocaml/setup-ocaml/lint-doc@v3 78 | -------------------------------------------------------------------------------- /.github/workflows/deploy-doc.yml: -------------------------------------------------------------------------------- 1 | name: deploy-doc 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # Review gh actions docs if you want to further define triggers, paths, etc 8 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on 9 | 10 | jobs: 11 | build: 12 | name: Build Docusaurus 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | shell: bash 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 18 27 | cache: npm 28 | cache-dependency-path: doc/package-lock.json 29 | 30 | - name: Install Docusaurus Dependencies 31 | working-directory: ./doc 32 | run: npm ci 33 | 34 | - name: Setup OCaml 35 | uses: ocaml/setup-ocaml@v3 36 | with: 37 | ocaml-compiler: "5.3.x" 38 | opam-repositories: | 39 | default: https://github.com/ocaml/opam-repository.git 40 | mbarbin: https://github.com/mbarbin/opam-repository.git 41 | # janestreet-bleeding: https://github.com/janestreet/opam-repository.git 42 | # janestreet-bleeding-external: https://github.com/janestreet/opam-repository.git#external-packages 43 | 44 | - name: Install OCaml Dependencies 45 | run: opam install . --deps-only --with-doc 46 | 47 | - name: Build Odoc Pages 48 | run: opam exec -- dune build @doc 49 | 50 | - name: Copy Odoc Pages to Docusaurus Static Directory 51 | run: | 52 | rm -rf doc/static/odoc 53 | cp -R _build/default/_doc/_html doc/static/odoc 54 | 55 | - name: Build Website 56 | working-directory: ./doc 57 | run: npm run build 58 | 59 | - name: Upload Build Artifact 60 | uses: actions/upload-pages-artifact@v3 61 | with: 62 | path: doc/build 63 | 64 | deploy: 65 | name: Deploy to GitHub Pages 66 | needs: build 67 | 68 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 69 | permissions: 70 | pages: write # to deploy to Pages 71 | id-token: write # to verify the deployment originates from an appropriate source 72 | 73 | # Deploy to the github-pages environment 74 | environment: 75 | name: github-pages 76 | url: ${{ steps.deployment.outputs.page_url }} 77 | 78 | runs-on: ubuntu-latest 79 | defaults: 80 | run: 81 | shell: bash 82 | working-directory: ./doc 83 | steps: 84 | - name: Deploy to GitHub Pages 85 | id: deployment 86 | uses: actions/deploy-pages@v4 87 | -------------------------------------------------------------------------------- /.github/workflows/edit_dune_project_dot_ml: -------------------------------------------------------------------------------- 1 | (* Usage: ocaml .github/workflows/edit_dune_project_dot_ml *) 2 | 3 | let starts_with s prefix = 4 | let len_s = String.length s in 5 | let len_p = String.length prefix in 6 | len_s >= len_p && String.sub s 0 len_p = prefix 7 | ;; 8 | 9 | let is_implicit_transitive_deps_line line = 10 | let prefix = "(implicit_transitive_deps" in 11 | starts_with (String.trim line) prefix 12 | ;; 13 | 14 | let () = 15 | let usage () = 16 | Printf.eprintf 17 | "Error: OCaml version argument required. Usage: %s \n" 18 | Sys.argv.(0); 19 | exit 1 20 | in 21 | if Array.length Sys.argv < 2 then usage (); 22 | let version = Sys.argv.(1) in 23 | let dune_project = "dune-project" in 24 | let file_lines = 25 | try 26 | let ic = open_in dune_project in 27 | let rec loop acc = 28 | match input_line ic with 29 | | line -> loop (line :: acc) 30 | | exception End_of_file -> List.rev acc 31 | in 32 | let lines = loop [] in 33 | close_in ic; 34 | lines 35 | with 36 | | Sys_error _ -> 37 | Printf.eprintf "File not found: %s\n" dune_project; 38 | exit 1 39 | in 40 | let major, minor = 41 | try 42 | match String.split_on_char '.' version with 43 | | major :: minor :: _ -> int_of_string major, int_of_string minor 44 | | _ -> failwith "Invalid version format" 45 | with 46 | | _ -> 47 | Printf.eprintf "Invalid OCaml version: %s\n" version; 48 | exit 1 49 | in 50 | let should_be_false = major > 5 || (major = 5 && minor >= 2) in 51 | let changed = ref false in 52 | let new_lines = 53 | List.map 54 | (fun line -> 55 | if is_implicit_transitive_deps_line line 56 | then ( 57 | changed := true; 58 | Printf.sprintf 59 | "(implicit_transitive_deps %s)" 60 | (if should_be_false then "false" else "true")) 61 | else line) 62 | file_lines 63 | in 64 | if !changed 65 | then ( 66 | let oc = open_out dune_project in 67 | List.iter 68 | (fun l -> 69 | output_string oc l; 70 | output_char oc '\n') 71 | new_lines; 72 | close_out oc) 73 | ;; 74 | -------------------------------------------------------------------------------- /.github/workflows/more-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow file is named 'more-ci' and is used to run additional CI checks 2 | # that complement the main CI workflow. It ensures that our code is tested 3 | # across multiple operating systems and OCaml compiler versions. 4 | # 5 | # Compared to the main 'ci.yml' job, this skips some steps that are not 6 | # necessary to check for every combination of os and ocaml-compiler, such as 7 | # generating coverage report, linting odoc, opam and fmt, etc. 8 | # 9 | # We prefer to keep it separate from the main CI workflow because we find it 10 | # more readable, over having too many conditional steps in the same job. 11 | 12 | name: more-ci 13 | 14 | on: 15 | push: 16 | branches: 17 | - main 18 | pull_request: 19 | branches: 20 | - "**" # This will match pull requests targeting any branch 21 | 22 | jobs: 23 | build: 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | os: 28 | - macos-latest 29 | - ubuntu-latest 30 | - windows-latest 31 | ocaml-compiler: 32 | - 5.3.x 33 | - 5.2.x 34 | - 4.14.x 35 | exclude: 36 | # We exclude the combination already tested in the 'ci' workflow. 37 | - os: ubuntu-latest 38 | ocaml-compiler: 5.3.x 39 | # We exclude windows-4.14 - this fails when building core. 40 | - os: windows-latest 41 | ocaml-compiler: 4.14.x 42 | 43 | runs-on: ${{ matrix.os }} 44 | 45 | steps: 46 | - name: Checkout code 47 | uses: actions/checkout@v4 48 | 49 | - name: Setup OCaml 50 | uses: ocaml/setup-ocaml@v3 51 | with: 52 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 53 | opam-repositories: | 54 | default: https://github.com/ocaml/opam-repository.git 55 | mbarbin: https://github.com/mbarbin/opam-repository.git 56 | # janestreet-bleeding: https://github.com/janestreet/opam-repository.git 57 | # janestreet-bleeding-external: https://github.com/janestreet/opam-repository.git#external-packages 58 | 59 | # Setting `(implicit_transitive_deps VALUE)` conditionally based on the compiler version. 60 | - name: Edit dune-project 61 | run: opam exec -- ocaml .github/workflows/edit_dune_project_dot_ml "${{ matrix.ocaml-compiler }}" 62 | 63 | # We build and run tests for a subset of packages. More tests are run in 64 | # the development workflow and as part of the main CI job. These are the 65 | # tests that are checked for every combination of os and ocaml-compiler. 66 | - name: Install dependencies 67 | run: opam install ./cmdlang.opam ./cmdlang-stdlib-runner.opam ./cmdlang-to-cmdliner.opam ./cmdlang-to-climate.opam --deps-only --with-test 68 | 69 | - name: Build & Run tests 70 | run: opam exec -- dune build @runtest -p cmdlang,cmdlang-stdlib-runner,cmdlang-to-cmdliner,cmdlang-to-climate 71 | -------------------------------------------------------------------------------- /.github/workflows/test-deploy-doc.yml: -------------------------------------------------------------------------------- 1 | name: test-deploy-doc 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | # Review gh actions docs if you want to further define triggers, paths, etc 8 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on 9 | 10 | jobs: 11 | test-deploy: 12 | name: Test deployment 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | shell: bash 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 18 27 | cache: npm 28 | cache-dependency-path: doc/package-lock.json 29 | 30 | - name: Install Docusaurus Dependencies 31 | working-directory: ./doc 32 | run: npm ci 33 | 34 | - name: Setup OCaml 35 | uses: ocaml/setup-ocaml@v3 36 | with: 37 | ocaml-compiler: "5.3.x" 38 | opam-repositories: | 39 | default: https://github.com/ocaml/opam-repository.git 40 | mbarbin: https://github.com/mbarbin/opam-repository.git 41 | # janestreet-bleeding: https://github.com/janestreet/opam-repository.git 42 | # janestreet-bleeding-external: https://github.com/janestreet/opam-repository.git#external-packages 43 | 44 | - name: Install OCaml Dependencies 45 | run: opam install . --deps-only --with-doc 46 | 47 | - name: Build Odoc Pages 48 | run: opam exec -- dune build @doc 49 | 50 | - name: Copy Odoc Pages to Docusaurus Static Directory 51 | run: | 52 | rm -rf doc/static/odoc 53 | cp -R _build/default/_doc/_html doc/static/odoc 54 | 55 | - name: Build Website 56 | working-directory: ./doc 57 | run: npm run build 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _opam 2 | _build 3 | _coverage 4 | *.install 5 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.27.0 2 | ocaml-version=4.14 3 | profile=janestreet 4 | parse-docstrings=true 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "cmdlang", 4 | "cmdliner", 5 | "conv", 6 | "docv", 7 | "Fpath", 8 | "groff", 9 | "janestreet", 10 | "odoc", 11 | "opam", 12 | "stdune", 13 | "stringable" 14 | ] 15 | } -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.0.10 (unreleased) 2 | 3 | ### Added 4 | 5 | - Test behavior when a group is called with an invalid subcommand (@mbarbin). 6 | 7 | ### Changed 8 | 9 | - Upgrade `climate` and now requires `>= 0.5.0` (@mbarbin). 10 | 11 | ### Deprecated 12 | 13 | ### Fixed 14 | 15 | ### Removed 16 | 17 | ## 0.0.9 (2024-11-30) 18 | 19 | ### Added 20 | 21 | - Added an example of migration from `core.command` to `climate` (#20, @mbarbin). 22 | - Added migration utils (#20, @mbarbin). 23 | - Improve code coverage, added tests (#20, @mbarbin). 24 | 25 | ### Changed 26 | 27 | - Document presence in stdlib-runner help (required, default, etc.) (#19, @mbarbin). 28 | - Minor refactor in stdlib-runner (#19, @mbarbin). 29 | - Upgrade to `climate.0.3.0` (#19, @mbarbin). 30 | 31 | ### Fixed 32 | 33 | - Fix trailing dot additions in `to-cmdliner` for cases such as `?.` and `..` (#19, @mbarbin). 34 | 35 | ### Removed 36 | 37 | - Removed config option `auto_add_short_aliases` from to-base translation (not useful) (#20, @mbarbin). 38 | 39 | ## 0.0.8 (2024-11-14) 40 | 41 | ### Added 42 | 43 | - Add more ci-checks: macOS, Windows, OCaml 4.14 (#17, @mbarbin). 44 | - Add a new backend based on `stdlib.arg` (#16, @mbarbin). 45 | 46 | ### Changed 47 | 48 | - Internal refactor to intermediate representations used in cmdlang-to-base (#16, @mbarbin). 49 | 50 | ### Fixed 51 | 52 | - Enable build with `ocaml.4.14` (#17, @mbarbin). 53 | 54 | ### Removed 55 | 56 | - Remove `Param.assoc`. We require now the `to_string` function found in `Enums` (#16, @mbarbin). 57 | 58 | ## 0.0.7 (2024-11-10) 59 | 60 | ### Removed 61 | 62 | - Moved `err`, `err-cli` and `cmdlang-cmdliner-runner` to [pp-log](https://github.com/mbarbin/pp-log). 63 | 64 | ## 0.0.6 (2024-10-24) 65 | 66 | ### Changed 67 | 68 | - Prepare documentation for initial release. 69 | - Upgrade to `climate.0.1.0`. 70 | - Make opam files pass opam-repository linting rules. 71 | - Upgrade Docusaurus. 72 | 73 | ## 0.0.5 (2024-09-17) 74 | 75 | ### Added 76 | 77 | - Expose `param` & `arg` translators. 78 | - Increase test coverage. 79 | 80 | ### Changed 81 | 82 | - Include `>>|` infix operator in `Command.Std`. 83 | - Separate the translation from the runner in 2 separate packages to keep dependencies isolated. 84 | 85 | ### Fixed 86 | 87 | - Fix handling of `docv` when translating to `core.command`. 88 | 89 | ### Removed 90 | 91 | - Removed most of applicative infix operators - keep only `>>|`. 92 | 93 | ## 0.0.4 (2024-09-07) 94 | 95 | ### Changed 96 | 97 | - Rename project `cmdlang`. 98 | 99 | ## 0.0.3 (2024-09-03) 100 | 101 | ### Changed 102 | 103 | - Refactor `Err` - undocumented changes while we're stabilizing. 104 | - Refactor the separation between `Err` and `Err_handler`. Keep only the cli part separate and rename it `err-cli`. 105 | 106 | ### Fixed 107 | 108 | - Fix some unintended behavior related to raising and catching errors with `err0` and `erro-handler`. Added tests to cover and characterize different use cases. 109 | 110 | ## 0.0.2 (2024-08-23) 111 | 112 | ### Changed 113 | 114 | - Make `cmdlang-err` and standalone library called `err0` so it can be used more broadly. Split the handler part as a separated lib `err0-handler`. 115 | 116 | ## 0.0.1 (2024-08-22) 117 | 118 | ### Added 119 | 120 | - Added library `Err` establishing a standard for error handling in cmdlang CLIs. 121 | 122 | ## 0.0.1_preview-0.1 (2024-08-19) 123 | 124 | ### Added 125 | 126 | - Added basic support for `readme`. 127 | - Added `Arg.named_multi`. 128 | - Added param helpers: `stringable`,`validated strings`, `comma_separated`. 129 | - Basic support for positional arguments. 130 | - Enabled instrumentation. 131 | - Adopted OCaml Code of Conduct. 132 | - Added a FAQ page. 133 | - Added test libraries. 134 | 135 | ### Changed 136 | 137 | - Internal changes to AST to make it more consistent. 138 | - Improve generation of man pages when using `cmdliner` as target. 139 | - Update tutorial to include positional arguments. 140 | 141 | ### Fixed 142 | 143 | - Translation to `core.command` requires `(unit -> _) Command.t` 144 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [OCaml Code of Conduct](https://github.com/ocaml/code-of-conduct/blob/main/CODE_OF_CONDUCT.md). 4 | 5 | # Enforcement 6 | 7 | This project follows the OCaml Code of Conduct [enforcement policy](https://github.com/ocaml/code-of-conduct/blob/main/CODE_OF_CONDUCT.md#enforcement). 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mathieu Barbin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: build 3 | 4 | .PHONY: build 5 | build: 6 | opam exec -- dune build 7 | 8 | .PHONY: test 9 | test: 10 | opam exec -- dune runtest 11 | 12 | .PHONY: fmt 13 | fmt: 14 | opam exec -- dune build @fmt --auto-promote 15 | 16 | .PHONY: lint 17 | lint: 18 | opam lint 19 | opam exec -- opam-dune-lint 20 | 21 | .PHONY: deps 22 | deps: 23 | opam install . --deps-only --with-doc --with-test --with-dev-setup 24 | 25 | .PHONY: doc 26 | doc: 27 | opam exec -- dune build @doc 28 | 29 | .PHONY: clean 30 | clean: 31 | opam exec -- dune clean 32 | 33 | .PHONY: check-all 34 | check-all: deps all test doc clean lint fmt 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmdlang 2 | 3 | [![CI Status](https://github.com/mbarbin/cmdlang/workflows/ci/badge.svg)](https://github.com/mbarbin/cmdlang/actions/workflows/ci.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/mbarbin/cmdlang/badge.svg?branch=main)](https://coveralls.io/github/mbarbin/cmdlang?branch=main) 5 | [![Deploy Doc Status](https://github.com/mbarbin/cmdlang/workflows/deploy-doc/badge.svg)](https://github.com/mbarbin/cmdlang/actions/workflows/deploy-doc.yml) 6 | 7 | Declarative Command-line Parsing for OCaml. 8 | 9 | ## Synopsis 10 | 11 | Cmdlang is a library for creating command-line parsers in OCaml. Implemented as an OCaml EDSL, its declarative specification language lives at the intersection of other well-established similar libraries. 12 | 13 | Cmdlang doesn't include an execution engine. Instead, Cmdlang parsers are automatically translated to `cmdliner`, `core.command`, or `climate` commands for execution. 14 | 15 | Our goal is to provide an approachable, flexible, and user-friendly interface while allowing users to choose the backend runtime that best suits their needs. 16 | 17 | ## Documentation 18 | 19 | Cmdlang's documentation is published [here](https://mbarbin.github.io/cmdlang). 20 | 21 | ## Rationale 22 | 23 | The OCaml community currently has two popular libraries for declarative command-line argument parsing: 24 | 25 | 1. [cmdliner](https://github.com/dbuenzli/cmdliner) 26 | 2. [core.command](https://github.com/janestreet/core), base's `Command` module. 27 | 28 | There is also a third library under development: 29 | 30 | 3. [climate](https://github.com/gridbugs/climate), aiming to support autocompletion scripts and other conventions. 31 | 32 | The following table reflects our understanding and preferences (ranked 1-3, most preferred first) as of the early days of `cmdlang` (your mileage may vary): 33 | 34 | | Library | Runtime (eval) | Ergonomic (mli) | CLI conventions | Man pages | Auto-complete | 35 | |----------------|:----------------------:|:-----------------:|:-----------------:|:----------:|:---------------:| 36 | | cmdliner | Battled-tested | 2 | 1 | Yes | No | 37 | | core.command | Battled-tested | 2 | 3 | No | Yes | 38 | | climate | (Under Construction) | 1 | 1 | No | Yes | 39 | 40 | **Programming Interface**: We find the type separation between the `Arg` & `Param` types proposed by `climate` to be the easiest to understand among the three models. 41 | 42 | **CLI syntax support**: Both `cmdliner` and `climate` support established conventions. We find that these conventions are harder to achieve with `core.command`, especially regarding its handling of long flag name arguments beginning with a single `-`. 43 | 44 | The `cmdlang` developers are enthusiastic to the prospect of compatible ways of working with these libraries. 45 | 46 | As developers of CLI tools written in OCaml, we aim to avoid a strong commitment to any single library if possible, especially concerning the runtime aspects. This is particularly relevant for new commands written today. 47 | 48 | In this spirit, we created `cmdlang`, a new library that offers a unique twist: it doesn't implement its own runtime. Instead, it translates its parsers into `cmdliner`, `core.command`, or `climate` parsers, making it compatible with all their execution engines. 49 | 50 | Our current preferred target is depicted below, but other combinations are possible: 51 | 52 | | Library | Runtime (eval) | Ergonomic (mli) | CLI conventions | Man pages | Auto-complete | 53 | |---------------|:----------------:|:---------------------:|:-----------------:|:-----------:|:---------------:| 54 | | cmdlang | Battled-tested | 1 | 1 | Yes | Yes* | 55 | | via | cmdliner | inspired by climate | cmdliner | cmdliner | Hybrid* | 56 | 57 | `*` Auto-completion: we plan to say more about it in the doc in the near future. 58 | 59 | Due to its architecture, `cmdlang` can be a helpful tool for implementing effective migration paths to transition from one of the existing libraries to another. 60 | 61 | We initiated the library as part of another project where we are migrating some commands from `core.command` to `cmdliner`, with the desire to make it easy to experiment with `climate` in the future. 62 | 63 | ## Architecture 64 | 65 | `cmdlang` is composed of several parts: 66 | 67 | 1. **Core Specification Language**: 68 | - A kernel command-line parsing specification language written as an OCaml EDSL. 69 | - Covers the intersection of what is expressible in `cmdliner`, `core.command`, and `climate`. 70 | 71 | 2. **OCaml Library**: 72 | - Exposes a single module, `Cmdlang.Command`, with no dependencies, to build command-line parsers in total abstraction using ergonomic helpers. 73 | - Supports various styles for writing command-lines, including `( let+ )` and `ppx_let`'s `let%map` or `let%map_open`. 74 | - Designed with ocaml-lsp in mind for user-friendly in-context completion. 75 | 76 | 3. **Translation Libraries**: 77 | - Convert `cmdlang` parsers at runtime into `cmdliner`, `core.command`, or `climate` parsers 78 | - Packaged as separate helper libraries to keep dependencies isolated. 79 | 80 | 4. **Basic execution runner based on stdlib.arg**: 81 | - A proof-of-concept execution engine implemented on top of `stdlib.arg`. 82 | 83 | ## Experimental Status 84 | 85 | `cmdlang` is currently under construction and considered experimental. We are actively seeking feedback to validate our design and engage with other declarative command-line enthusiasts. 86 | 87 | ## Acknowledgements 88 | 89 | - We are grateful for the years of accumulated work and experience that have resulted in high-quality CLI libraries like `cmdliner` and `core.command`. 90 | - `climate`'s early programming interface was a great source of inspiration. We are very thankful for their work on auto-completion and excited to see where the `climate` project goes next. 91 | - We are inspired by the [diataxis](https://diataxis.fr/) approach to technical documentation, which we use to structure our documentation. 92 | -------------------------------------------------------------------------------- /cmdlang-stdlib-runner.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "A basic execution runner for cmdlang based on stdlib.arg" 4 | maintainer: ["Mathieu Barbin "] 5 | authors: ["Mathieu Barbin"] 6 | license: "MIT" 7 | homepage: "https://github.com/mbarbin/cmdlang" 8 | doc: "https://mbarbin.github.io/cmdlang/" 9 | bug-reports: "https://github.com/mbarbin/cmdlang/issues" 10 | depends: [ 11 | "dune" {>= "3.17"} 12 | "ocaml" {>= "4.14"} 13 | "cmdlang" {= version} 14 | "odoc" {with-doc} 15 | ] 16 | build: [ 17 | ["dune" "subst"] {dev} 18 | [ 19 | "dune" 20 | "build" 21 | "-p" 22 | name 23 | "-j" 24 | jobs 25 | "@install" 26 | "@runtest" {with-test} 27 | "@doc" {with-doc} 28 | ] 29 | ] 30 | dev-repo: "git+https://github.com/mbarbin/cmdlang.git" 31 | description: """\ 32 | 33 | [Cmdlang_stdlib_runner] is an execution engine for running command 34 | line programs specified with [cmdlang]. 35 | 36 | It has no dependencies other than [cmdlang] and is implemented using 37 | the [Arg] module from the OCaml standard library. 38 | 39 | This package may be useful as a lightweight alternative to translating 40 | cmdlang parsers to more feature-rich libraries such as [cmdliner], 41 | [climate], or [core.command]. 42 | 43 | """ 44 | tags: [ "cli" "cmdlang" "stdlib.arg" ] 45 | x-maintenance-intent: [ "(latest)" ] 46 | -------------------------------------------------------------------------------- /cmdlang-stdlib-runner.opam.template: -------------------------------------------------------------------------------- 1 | description: """\ 2 | 3 | [Cmdlang_stdlib_runner] is an execution engine for running command 4 | line programs specified with [cmdlang]. 5 | 6 | It has no dependencies other than [cmdlang] and is implemented using 7 | the [Arg] module from the OCaml standard library. 8 | 9 | This package may be useful as a lightweight alternative to translating 10 | cmdlang parsers to more feature-rich libraries such as [cmdliner], 11 | [climate], or [core.command]. 12 | 13 | """ 14 | tags: [ "cli" "cmdlang" "stdlib.arg" ] 15 | x-maintenance-intent: [ "(latest)" ] 16 | -------------------------------------------------------------------------------- /cmdlang-tests.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Tests for cmdlang" 4 | maintainer: ["Mathieu Barbin "] 5 | authors: ["Mathieu Barbin"] 6 | license: "MIT" 7 | homepage: "https://github.com/mbarbin/cmdlang" 8 | doc: "https://mbarbin.github.io/cmdlang/" 9 | bug-reports: "https://github.com/mbarbin/cmdlang/issues" 10 | depends: [ 11 | "dune" {>= "3.17"} 12 | "ocaml" {>= "5.2"} 13 | "ocamlformat" {with-dev-setup & = "0.27.0"} 14 | "base" {>= "v0.17"} 15 | "bisect_ppx" {with-dev-setup & >= "2.8.3"} 16 | "climate" {>= "0.5.0"} 17 | "cmdlang" {= version} 18 | "cmdlang-stdlib-runner" {= version} 19 | "cmdlang-to-base" {= version} 20 | "cmdlang-to-climate" {= version} 21 | "cmdlang-to-cmdliner" {= version} 22 | "cmdliner" {>= "1.3.0"} 23 | "core" {>= "v0.17"} 24 | "core_unix" {>= "v0.17"} 25 | "expect_test_helpers_core" {>= "v0.17"} 26 | "loc" {>= "0.2.2"} 27 | "mdx" {>= "2.4"} 28 | "ppx_compare" {>= "v0.17"} 29 | "ppx_enumerate" {>= "v0.17"} 30 | "ppx_expect" {>= "v0.17"} 31 | "ppx_hash" {>= "v0.17"} 32 | "ppx_here" {>= "v0.17"} 33 | "ppx_js_style" {with-dev-setup & >= "v0.17"} 34 | "ppx_let" {>= "v0.17"} 35 | "ppx_sexp_conv" {>= "v0.17"} 36 | "ppx_sexp_value" {>= "v0.17"} 37 | "ppxlib" {>= "0.33"} 38 | "stdio" {>= "v0.17"} 39 | "stdune" {>= "3.17"} 40 | "sherlodoc" {with-doc & >= "0.2"} 41 | "odoc" {with-doc} 42 | ] 43 | build: [ 44 | ["dune" "subst"] {dev} 45 | [ 46 | "dune" 47 | "build" 48 | "-p" 49 | name 50 | "-j" 51 | jobs 52 | "@install" 53 | "@runtest" {with-test} 54 | "@doc" {with-doc} 55 | ] 56 | ] 57 | dev-repo: "git+https://github.com/mbarbin/cmdlang.git" 58 | -------------------------------------------------------------------------------- /cmdlang-tests.opam.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/cmdlang-tests.opam.template -------------------------------------------------------------------------------- /cmdlang-to-base.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Convert cmdlang Parsers to core.command" 4 | maintainer: ["Mathieu Barbin "] 5 | authors: ["Mathieu Barbin"] 6 | license: "MIT" 7 | homepage: "https://github.com/mbarbin/cmdlang" 8 | doc: "https://mbarbin.github.io/cmdlang/" 9 | bug-reports: "https://github.com/mbarbin/cmdlang/issues" 10 | depends: [ 11 | "dune" {>= "3.17"} 12 | "ocaml" {>= "5.2"} 13 | "base" {>= "v0.17"} 14 | "cmdlang" {= version} 15 | "core" {>= "v0.17"} 16 | "ppx_compare" {>= "v0.17"} 17 | "ppx_enumerate" {>= "v0.17"} 18 | "ppx_expect" {>= "v0.17"} 19 | "ppx_hash" {>= "v0.17"} 20 | "ppx_here" {>= "v0.17"} 21 | "ppx_let" {>= "v0.17"} 22 | "ppx_sexp_conv" {>= "v0.17"} 23 | "ppx_sexp_value" {>= "v0.17"} 24 | "ppxlib" {>= "0.33"} 25 | "stdio" {>= "v0.17"} 26 | "odoc" {with-doc} 27 | ] 28 | build: [ 29 | ["dune" "subst"] {dev} 30 | [ 31 | "dune" 32 | "build" 33 | "-p" 34 | name 35 | "-j" 36 | jobs 37 | "@install" 38 | "@runtest" {with-test} 39 | "@doc" {with-doc} 40 | ] 41 | ] 42 | dev-repo: "git+https://github.com/mbarbin/cmdlang.git" 43 | description: """\ 44 | 45 | [Cmdlang_to_base] allows translating command line programs specified 46 | with [cmdlang] into [core.command] commands suitable for execution. 47 | 48 | [core.command]: https://github.com/janestreet/core 49 | 50 | """ 51 | tags: [ "cli" "cmdlang" "core.command" ] 52 | x-maintenance-intent: [ "(latest)" ] 53 | -------------------------------------------------------------------------------- /cmdlang-to-base.opam.template: -------------------------------------------------------------------------------- 1 | description: """\ 2 | 3 | [Cmdlang_to_base] allows translating command line programs specified 4 | with [cmdlang] into [core.command] commands suitable for execution. 5 | 6 | [core.command]: https://github.com/janestreet/core 7 | 8 | """ 9 | tags: [ "cli" "cmdlang" "core.command" ] 10 | x-maintenance-intent: [ "(latest)" ] 11 | -------------------------------------------------------------------------------- /cmdlang-to-climate.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Convert cmdlang Parsers to climate" 4 | maintainer: ["Mathieu Barbin "] 5 | authors: ["Mathieu Barbin"] 6 | license: "MIT" 7 | homepage: "https://github.com/mbarbin/cmdlang" 8 | doc: "https://mbarbin.github.io/cmdlang/" 9 | bug-reports: "https://github.com/mbarbin/cmdlang/issues" 10 | depends: [ 11 | "dune" {>= "3.17"} 12 | "ocaml" {>= "4.14"} 13 | "climate" {>= "0.5.0"} 14 | "cmdlang" {= version} 15 | "odoc" {with-doc} 16 | ] 17 | build: [ 18 | ["dune" "subst"] {dev} 19 | [ 20 | "dune" 21 | "build" 22 | "-p" 23 | name 24 | "-j" 25 | jobs 26 | "@install" 27 | "@runtest" {with-test} 28 | "@doc" {with-doc} 29 | ] 30 | ] 31 | dev-repo: "git+https://github.com/mbarbin/cmdlang.git" 32 | description: """\ 33 | 34 | [Cmdlang_to_climate] allows translating command line programs 35 | specified with [cmdlang] into [climate] commands suitable for 36 | execution. 37 | 38 | [climate]: https://github.com/gridbugs/climate 39 | 40 | """ 41 | tags: [ "cli" "cmdlang" "climate" ] 42 | x-maintenance-intent: [ "(latest)" ] 43 | -------------------------------------------------------------------------------- /cmdlang-to-climate.opam.template: -------------------------------------------------------------------------------- 1 | description: """\ 2 | 3 | [Cmdlang_to_climate] allows translating command line programs 4 | specified with [cmdlang] into [climate] commands suitable for 5 | execution. 6 | 7 | [climate]: https://github.com/gridbugs/climate 8 | 9 | """ 10 | tags: [ "cli" "cmdlang" "climate" ] 11 | x-maintenance-intent: [ "(latest)" ] 12 | -------------------------------------------------------------------------------- /cmdlang-to-cmdliner.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Convert cmdlang Parsers to cmdliner" 4 | maintainer: ["Mathieu Barbin "] 5 | authors: ["Mathieu Barbin"] 6 | license: "MIT" 7 | homepage: "https://github.com/mbarbin/cmdlang" 8 | doc: "https://mbarbin.github.io/cmdlang/" 9 | bug-reports: "https://github.com/mbarbin/cmdlang/issues" 10 | depends: [ 11 | "dune" {>= "3.17"} 12 | "ocaml" {>= "4.14"} 13 | "cmdlang" {= version} 14 | "cmdliner" {>= "1.3.0"} 15 | "odoc" {with-doc} 16 | ] 17 | build: [ 18 | ["dune" "subst"] {dev} 19 | [ 20 | "dune" 21 | "build" 22 | "-p" 23 | name 24 | "-j" 25 | jobs 26 | "@install" 27 | "@runtest" {with-test} 28 | "@doc" {with-doc} 29 | ] 30 | ] 31 | dev-repo: "git+https://github.com/mbarbin/cmdlang.git" 32 | description: """\ 33 | 34 | [Cmdlang_to_cmdliner] allows translating command line programs 35 | specified with [cmdlang] into [cmdliner] commands suitable for 36 | execution. 37 | 38 | [cmdliner]: https://github.com/dbuenzli/cmdliner 39 | 40 | """ 41 | tags: [ "cli" "cmdlang" "cmdliner" ] 42 | x-maintenance-intent: [ "(latest)" ] 43 | -------------------------------------------------------------------------------- /cmdlang-to-cmdliner.opam.template: -------------------------------------------------------------------------------- 1 | description: """\ 2 | 3 | [Cmdlang_to_cmdliner] allows translating command line programs 4 | specified with [cmdlang] into [cmdliner] commands suitable for 5 | execution. 6 | 7 | [cmdliner]: https://github.com/dbuenzli/cmdliner 8 | 9 | """ 10 | tags: [ "cli" "cmdlang" "cmdliner" ] 11 | x-maintenance-intent: [ "(latest)" ] 12 | -------------------------------------------------------------------------------- /cmdlang.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Declarative Command-line Parsing for OCaml" 4 | maintainer: ["Mathieu Barbin "] 5 | authors: ["Mathieu Barbin"] 6 | license: "MIT" 7 | homepage: "https://github.com/mbarbin/cmdlang" 8 | doc: "https://mbarbin.github.io/cmdlang/" 9 | bug-reports: "https://github.com/mbarbin/cmdlang/issues" 10 | depends: [ 11 | "dune" {>= "3.17"} 12 | "ocaml" {>= "4.14"} 13 | "odoc" {with-doc} 14 | ] 15 | build: [ 16 | ["dune" "subst"] {dev} 17 | [ 18 | "dune" 19 | "build" 20 | "-p" 21 | name 22 | "-j" 23 | jobs 24 | "@install" 25 | "@runtest" {with-test} 26 | "@doc" {with-doc} 27 | ] 28 | ] 29 | dev-repo: "git+https://github.com/mbarbin/cmdlang.git" 30 | description: """\ 31 | 32 | Cmdlang is a library for creating command-line parsers in OCaml. 33 | Implemented as an OCaml EDSL, its declarative specification language 34 | lives at the intersection of other well-established similar libraries. 35 | 36 | Cmdlang doesn't include an execution engine. Instead, Cmdlang parsers 37 | are automatically translated to [cmdliner], [core.command], or 38 | [climate] commands for execution. 39 | 40 | [cmdliner]: https://github.com/dbuenzli/cmdliner 41 | [climate]: https://github.com/gridbugs/climate 42 | [core.command]: https://github.com/janestreet/core 43 | 44 | """ 45 | tags: [ "cli" ] 46 | x-maintenance-intent: [ "(latest)" ] 47 | -------------------------------------------------------------------------------- /cmdlang.opam.template: -------------------------------------------------------------------------------- 1 | description: """\ 2 | 3 | Cmdlang is a library for creating command-line parsers in OCaml. 4 | Implemented as an OCaml EDSL, its declarative specification language 5 | lives at the intersection of other well-established similar libraries. 6 | 7 | Cmdlang doesn't include an execution engine. Instead, Cmdlang parsers 8 | are automatically translated to [cmdliner], [core.command], or 9 | [climate] commands for execution. 10 | 11 | [cmdliner]: https://github.com/dbuenzli/cmdliner 12 | [climate]: https://github.com/gridbugs/climate 13 | [core.command]: https://github.com/janestreet/core 14 | 15 | """ 16 | tags: [ "cli" ] 17 | x-maintenance-intent: [ "(latest)" ] 18 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /doc/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /doc/blog/2024-08-07-hello/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hello 3 | title: Hello 4 | authors: [mbarbin] 5 | tags: [hello] 6 | --- 7 | 8 | Hello! I've just launched a blog section within the cmdlang documentation, powered by Docusaurus. This new space is designed to keep you updated with all things related to cmdlang. Stay tuned for more updates and insights. Happy reading! 9 | 10 | 11 | -------------------------------------------------------------------------------- /doc/blog/2024-09-07-yet-another-cli/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: introducing-cmdlang 3 | title: Yet Another CLI Library (well, not really) 4 | authors: [mbarbin] 5 | tags: [cmdlang, climate, cmdliner, core.command, stdlib.arg] 6 | --- 7 | 8 | https://discuss.ocaml.org/t/cmdlang-yet-another-cli-library-well-not-really/15258 9 | 10 | Greetings fellow camlers, 11 | 12 | I hope you had a nice summer! Mine took an unexpected turn when, roughly at the same time, I wrote my first `cmdliner` subcommand and heard about `climate` for the first time. My experience with OCaml CLI so far had been centered around `core.command`. 13 | 14 | When I read climate's [terminology](https://github.com/gridbugs/climate?tab=readme-ov-file#terminology) section and how it defines `Terms`, `Arguments`, and `Parameters`, something clicked. Seeing how `climate`'s API managed to make positional and named arguments fit so nicely together, I thought: "Wow, for the first time, it seems I'll be able to write a CLI spec on a whiteboard without referring to some code I never seem to get right (I am looking at you, core.command's anonymous arguments)." 15 | 16 | 17 | 18 | I got quite excited and thought: "Can I switch to `climate` today?" But reality checked: it's not on opam yet, still under construction, I'm not sure what the community will do, etc. 19 | 20 | Implementing my own engine for an API resembling `climate` felt like a wasted effort, knowing about the work happening in `climate`. Still, having a `'a Param.t`, `'a Arg.t`, and `'a Command.t` type that I would get to know and love felt too good to pass up. 21 | 22 | I stared at the `climate` types for a while, and filled with happy thoughts about a bright CLI future, it occurred to me: can I use an API like `climate` but compile it down to existing libraries such as `cmdliner` or `core.command`? (and `climate` too!). I wrote down the following types: 23 | 24 | **climate** 25 | 26 | ```ocaml 27 | 'a Param.t -> 'a Climate.Arg_parser.conv 28 | 'a Arg.t -> 'a Climate.Arg_parser.t 29 | 'a Command.t -> 'a Climate.Command.t 30 | ``` 31 | 32 | **cmdliner** 33 | 34 | ```ocaml 35 | 'a Param.t -> 'a Cmdliner.Arg.conv 36 | 'a Arg.t -> 'a Cmdliner.Term.t 37 | 'a Command.t -> 'a Cmdliner.Cmd.t 38 | ``` 39 | 40 | **core.command** 41 | 42 | ```ocaml 43 | 'a Param.t -> 'a core.Command.Arg_type.t 44 | 'a Arg.t -> 'a core.Command.Param.t 45 | unit Command.t -> core.Command.t 46 | ``` 47 | 48 | ... which I interpreted as stating the following theorem: 49 | 50 | > There exists an abstraction to encode OCaml CLIs that lives in the intersection of what's expressible in other well established libraries. 51 | 52 | "One EDSL to command them all," so to speak. I couldn't resist the temptation to build actual terms for these types. That gave birth to [cmdlang](https://github.com/mbarbin/cmdlang). 53 | 54 | As a test, I switched one of my projects to `cmdlang`, with `cmdliner` as a backend. I liked the [changes](https://github.com/mbarbin/bopkit/pull/14) I made in the process. The 1-line [bin/main.ml](https://github.com/mbarbin/bopkit/blob/main/bin/main.ml) is now the only place that specifies which backend I want to use; the rest of the code is programmed solely against the `cmdlang` API. This means I'll be able to easily experiment with compiling down to `climate` in the future. 55 | 56 | I am not against the multiplicity of solutions in general, but I tend to feel uneasy when incompatible libraries emerge, partitioning the ecosystem. As a community, we know too many examples of this. In this instance, I want to call the `core.command` vs `cmdliner` situation a ... cli-vage. 57 | 58 | I don't see my work on `cmdlang` as competing with these other libraries. Quite the contrary, it makes it easier for me to experiment with them without much changes while exploring the subject of CLI in general. Also, as a library author, if you wish to expose CLI helpers to your users, a library like `cmdlang` will give you a pleasant way to do so, as you can express your helpers with it, knowing your users will have the choice to translate them to the backend of their choice. 59 | 60 | Before writing this post, I had a very pleasant chat with @gridbugs. I want to make it clear that I do not think `cmdlang` is competing with `climate` either. I think `climate` is a very promising library and I believe it will, in due time, deliver auto-completion to many - this has been a highly anticipated feature within the community. I wish to dedicate the initial work that I did on `cmdlang` to @gridbugs due to the impactful influence climate had on my work, and how it helped me improve my general understanding of declarative CLI libraries. 61 | 62 | These are very early days for `cmdlang`. There are still areas I am fuzzy on, and I haven't really validated the whole design yet. I have put some thoughts in this [Future Plans](https://mbarbin.github.io/cmdlang/docs/explanation/future_plans/) page. One thing that I did not initially include there would be to explore the feasibility of writing a mini-compiler for `cmdlang` targeting `stdlib.arg` as a runner. I am not sure how much you'd end up restricting `cmdlang` for this to work. I thought that'd be a fun project to tackle at a future point, as it would make a nice addition to the overall architecture of the project. 63 | 64 | I'd welcome your input to help me shape the future of `cmdlang` if you have an interest in this project. 65 | 66 | Thanks for reading! 67 | -------------------------------------------------------------------------------- /doc/blog/2024-11-15-first-release/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: first-release 3 | title: First release of cmdlang 4 | authors: [mbarbin] 5 | tags: [cmdlang, stdlib.arg] 6 | --- 7 | 8 | https://discuss.ocaml.org/t/first-release-of-cmdlang/15616 9 | 10 | Hi everyone! 11 | 12 | A little while ago, I [posted](https://discuss.ocaml.org/t/cmdlang-yet-another-cli-library-well-not-really/15258) about [cmdlang](https://github.com/mbarbin/cmdlang), a library for creating command-line parsers in OCaml. 13 | 14 | Today, I am happy to give you an update on this project with the announcement of an initial release of cmdlang packages to the opam-repository. 15 | 16 | These are very early days for this project. I have started using the `cmdlang+cmdliner` combination in personal projects, and plan to experiment with `climate` in the near future. Please feel free to engage in issues/discussions, etc. 17 | 18 | 19 | 20 | The most recent addition on the project is the development of an evaluation engine based on `stdlib/arg`. 21 | 22 | I'd also like to highlight some examples from the project's tests. Developing these characterization tests was a fun way to learn more about the different CLI libraries and their differences: 23 | 24 | - Short, long and prefix [flag names](https://github.com/mbarbin/cmdlang/blob/main/test/expect/test__flag.ml). 25 | 26 | - Various syntaxes for [named arguments](https://github.com/mbarbin/cmdlang/blob/main/test/expect/test__named.ml) (`-pVALUE`, `-p=VALUE`, `-p VALUE`). 27 | 28 | - Handling of [negative integers](https://github.com/mbarbin/cmdlang/blob/main/test/expect/test__negative_int_args.ml) as named arguments. 29 | 30 | If you have ideas for more cases to add (entertaining or otherwise), I'd love to integrate them into the test suite. Thanks! 31 | 32 | Below, you'll find details of the released packages. Happy command parsing! 33 | 34 | **cmdlang** the user facing library to build the commands. It has no dependencies 35 | 36 | **cmdlang-to-cmdliner** translate cmdlang commands to cmdliner 37 | 38 | **cmdlang-to-climate** translate cmdlang commands to the newly released climate (compatibility checked with 0.1.0 & 0.2.0) 39 | 40 | **cmdlang-stdlib-runner** an execution engine implemented on top of stdlib.arg 41 | 42 | Thank you to @mseri and the opam-repository maintainers for their help. 43 | -------------------------------------------------------------------------------- /doc/blog/authors.yml: -------------------------------------------------------------------------------- 1 | mbarbin: 2 | name: Mathieu Barbin 3 | title: Author & Maintainer of cmdlang 4 | url: https://github.com/mbarbin 5 | image_url: https://github.com/mbarbin.png 6 | -------------------------------------------------------------------------------- /doc/blog/tags.yml: -------------------------------------------------------------------------------- 1 | hello: 2 | label: Hello 3 | permalink: /hello 4 | description: Hello tag description 5 | 6 | cmdlang: 7 | label: cmdlang 8 | permalink: /cmdlang 9 | description: cmdlang 10 | 11 | cmdliner: 12 | label: cmdliner 13 | permalink: /cmdliner 14 | description: cmdliner 15 | 16 | climate: 17 | label: climate 18 | permalink: /climate 19 | description: climate 20 | 21 | core.command: 22 | label: core.command 23 | permalink: /core.command 24 | description: core.command 25 | 26 | stdlib.arg: 27 | label: stdlib.arg 28 | permalink: /stdlib.arg 29 | description: stdlib.arg 30 | -------------------------------------------------------------------------------- /doc/docs/explanation/README.md: -------------------------------------------------------------------------------- 1 | # Explanation 2 | 3 | Welcome to the Explanation section of the `cmdlang` documentation. Here, we delve into the details of how `cmdlang` works, its design principles, and our future plans. This section is intended to provide a deeper understanding of the project for developers and contributors. 4 | 5 | ## Architecture 6 | 7 | `cmdlang` is composed of several parts: 8 | 9 | - **Core Specification Language**: A kernel command-line parsing specification language written as an OCaml EDSL. 10 | - **OCaml Library**: Exposes a single module, `Cmdlang.Command`, with no dependencies, to build command-line parsers in total abstraction using ergonomic helpers. 11 | - **Translation Libraries**: Convert `cmdlang` parsers at runtime into `cmdliner`, `core.command`, or `climate` parsers. 12 | - **Basic execution runner**: A proof-of-concept execution engine implemented on top of `stdlib.arg`. 13 | 14 | ## Experimental Status 15 | 16 | `cmdlang` is currently under construction and considered experimental. We are actively seeking feedback to validate our design and engage with other declarative command-line enthusiasts. 17 | 18 | ## Future Plans 19 | 20 | In this section, we outline some areas of uncertainty regarding the feasibility of our design in detail. These are the areas we plan to explore next: 21 | 22 | 1. **Anonymous Arguments (Positional Arguments)** 23 | 2. **Generation of Complex Man Pages** 24 | 3. **Auto-Completion** 25 | 26 | For more details, refer to the [Future Plans](./future_plans.md) section. 27 | 28 | ## Acknowledgements 29 | 30 | - We are grateful for the years of accumulated work and experience that have resulted in high-quality CLI libraries like `cmdliner` and `core.command` 31 | - `climate`'s early programming interface was a great source of inspiration. We are very thankful for their work on auto-completion and excited to see where the `climate` project goes next. 32 | 33 | For more details, refer to the Acknowledgements section of the project on GitHub. 34 | -------------------------------------------------------------------------------- /doc/docs/explanation/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Why Questions 4 | 5 | #### Why aren't there more helpers exposed? 6 | 7 | We are currently in an exploratory phase and need to easily update our internal representations. Adding too many helpers now would complicate this process. Therefore, we aim to balance providing enough functionality for early trials while keeping the core minimal. We plan to revisit this later with user feedback. 8 | 9 | #### Why isn't Cmdlang's Arg parser an arrow? 10 | 11 | We are uncertain if all targeted models support branching. When writing a CLI for an OCaml program, encoding variants in the CLI can create a clear mapping between the user interface and the internal implementation. However, this approach might complicate the CLI. Cmdlang aims to operate at the intersection of its targeted backends, which could be a significant constraint if you need functionality only offered in a specific backend. This is an area for future development. If you need a feature that is only available in another CLI library, cmdlang might not be a good fit for your project. 12 | 13 | ## Other Questions 14 | 15 | Feel free to ask any other questions about cmdlang. 16 | -------------------------------------------------------------------------------- /doc/docs/explanation/future_plans.md: -------------------------------------------------------------------------------- 1 | # Future Plans 2 | 3 | In this section, we outline some areas of uncertainty regarding the feasibility of our design in detail. These are the areas we plan to explore next. 4 | 5 | ## Positional Arguments 6 | 7 | *Also known as anonymous arguments in `core.command`.* 8 | 9 | One of the key areas we need to investigate further is the handling of anonymous arguments, also known as positional arguments in `cmdliner` and `climate`. Due to the differences in how these arguments are managed in `core.command`, translating them may not be as straightforward as it is for named arguments. 10 | 11 | However, we have a good intuition that by reducing some of the expressiveness of what we allow to construct, we can solve cases that are sufficient in practice. For example, enforcing that positional arguments be defined in left-to-right order in the specification might be a viable approach. 12 | 13 | ### Tasks 14 | - [x] Investigate handling of anonymous arguments in `cmdliner` and `climate` (Completed: Aug 2024) 15 | - [x] Develop a strategy for translating positional arguments in `core.command` (Completed: Aug 2024) 16 | - [x] Implement left-to-right order enforcement for positional arguments when compiling to `core.command` (Completed: Aug 2024) 17 | 18 | ## Targeting stdlib.arg as a runner 19 | 20 | We'd like to write a mini-compiler targeting `stdlib.arg` as a proof-of-concept showing that it is possible to implement an execution runner for cmdlang that reuses the parsing engine implemented in the standard library. 21 | 22 | ### Tasks 23 | - [x] Implemented an execution engine for cmdlang based on `stdlib.arg` (Completed: Nov 2024) 24 | 25 | ## Generation of Complex Man Pages 26 | 27 | Another area of focus is the generation of complex man pages. `cmdliner` has excellent support for these. Currently, we have added basic support for one-line summaries of help messages to get started. However, we believe we could reuse most of the design of `cmdliner` and add it as optional information to the specification language. 28 | 29 | The rendering to simple strings could be exported by a standalone `cmdliner` helper library to reduce dependencies. We could then reuse and integrate this in the translation to `core.command` and `climate`. This part is more prospective and may require some coordination with the developers of other libraries. 30 | 31 | ### Tasks 32 | - [x] Add support for one-line summaries and readme help pages. 33 | - [ ] Design optional information for specification language based on `cmdliner` 34 | - [ ] Create a standalone helper library for rendering to simple strings 35 | - [ ] Integrate design into translation to `core.command` and `climate` 36 | 37 | ## Auto-Completion 38 | 39 | The third point is by far the most challenging but also a source of significant added value: auto-completion. This has been a highly anticipated feature within the community, and we have recently seen progress in this area, particularly with the `climate` developers working on integrating support for auto-completion. 40 | 41 | We envision a potential hybrid translation mode where a `cmdlang` command is translated into both `cmdliner` and `climate` — `cmdliner` for runtime execution and `climate` to generate completion scripts. This approach leaves the question of reentrant completion as future work. 42 | 43 | Given the ongoing developments in other libraries regarding auto-completion, we will carefully consider how `cmdlang`'s unique architecture can accommodate and leverage these changes. This will require thoughtful planning and possibly significant adjustments as the landscape evolves. 44 | 45 | ### Tasks 46 | - [ ] Research auto-completion support in `climate` 47 | - [ ] Develop a hybrid translation mode for `cmdlang` commands 48 | - [ ] Implement auto-completion feature 49 | -------------------------------------------------------------------------------- /doc/docs/guides/usage-styles/README.md: -------------------------------------------------------------------------------- 1 | # Usage Styles 2 | 3 | This section demonstrates how to use the helper modules available in `cmdlang` and how to add them to the scope using different styles commonly used in the OCaml community. 4 | 5 | Depending on your preference and the conventions of your project, you can choose from various styles to incorporate `cmdlang` into your code. The `Command.Std` module provides a convenient way to access all the standard components of `cmdlang`, while directly using `Cmdlang`, local open statements, or aliases can offer more explicit control over the scope. 6 | 7 | ## Introducing Command to the scope 8 | 9 | To define commands, you need to import the `cmdlang` package. This packages defines an OCaml library named `Cmdlang`, which exports a single module named `Command`. There are several ways `Command` may be brought to scope. 10 | 11 | ### Import via flags 12 | 13 | The approach recommended by the `cmdlang` authors is to avoid mentioning the name `cmdlang` directly in your OCaml files. Instead, configure the project dependencies to open `Cmdlang` via flags in your dune file: 14 | 15 | 16 | ```lisp 17 | (library 18 | (name my_library) 19 | (flags :standard -open Cmdlang) 20 | (libraries cmdlang)) 21 | ``` 22 | 23 | However, other options are possible. For example, you can use an import file to specify module aliases: 24 | 25 | ### Import file 26 | 27 | A common practice consists in having an `import.ml` file per directory, where you can specify module aliases. 28 | 29 | 30 | ```ocaml 31 | module Command = Cmdlang.Command 32 | ``` 33 | 34 | Then, each other file in the directory starts with: 35 | 36 | 37 | ```ocaml 38 | open! Import 39 | ``` 40 | 41 | Setup that way, the module `Command` is effectively bound to `Cmdlang.Command` in all the files. 42 | 43 | ### Local aliases 44 | 45 | If your project doesn't use import files, you can always move this alias near the parts where the command are defined. 46 | 47 | 48 | ```ocaml 49 | module Command = Cmdlang.Command 50 | 51 | (* Define your commands below. *) 52 | ``` 53 | 54 | In the rest of the guide, we'll assume that the module is available as `Command`. 55 | 56 | ## Declarative styles 57 | 58 | ### Using let+ and let open 59 | 60 | In this style, the applicative syntax part is implemented via the use of `let+` and `and+` [binding operators](https://ocaml.org/manual/5.2/bindingops.html). 61 | 62 | They are introduced to the scope, alongside the common modules `Arg` and `Param` via a local open of `Command.Std`. 63 | 64 | 65 | ```ocaml 66 | let _ : unit Command.t = 67 | Command.make 68 | ~summary:"A command skeleton" 69 | (let open Command.Std in 70 | let+ (_ : int) = Arg.named [ "n" ] Param.int ~doc:"A value for n" 71 | and+ () = Arg.return () in 72 | ()) 73 | ;; 74 | ``` 75 | 76 | This is the main style recommended by the cmdlang authors. 77 | 78 | #### No Indentation Tweak with @@ 79 | 80 | Some people prefer limiting the indentation of large blocks with the help of the infix operator `@@`. In this context, this may look like this: 81 | 82 | 83 | ```ocaml 84 | let _ : unit Command.t = 85 | Command.make ~summary:"A command skeleton" 86 | @@ 87 | let open Command.Std in 88 | let+ (_ : int) = Arg.named [ "n" ] Param.int ~doc:"A value for n" 89 | and+ () = Arg.return () in 90 | () 91 | ;; 92 | ``` 93 | 94 | The cmdlang authors do not have much experience with this style at the time of writing. 95 | 96 | ### Using let-syntax and map_open 97 | 98 | An alternative based on the `let%map_open` operator of [ppx_let](https://github.com/janestreet/ppx_let) is also supported. 99 | 100 | 101 | ```ocaml 102 | let _ : unit Command.t = 103 | Command.make 104 | ~summary:"A command skeleton" 105 | (let%map_open.Command () = Arg.return () 106 | and () = Arg.return () in 107 | ()) 108 | ;; 109 | ``` 110 | 111 | ### Other styles 112 | 113 | `cmdlang` aims to be flexible and accommodate various coding styles. If you have a specific style or use case that is not covered here, please reach out to us. We are open to feedback and can make adjustments to the interface to better support your needs. 114 | 115 | ## Conclusion 116 | 117 | This guide has shown you different ways to use the helper modules in `cmdlang`, including a standard recommended by the authors. Feel free to experiment with these styles and choose the one that best suits your project. 118 | -------------------------------------------------------------------------------- /doc/docs/guides/usage-styles/dune: -------------------------------------------------------------------------------- 1 | (mdx 2 | (package cmdlang-tests) 3 | (deps 4 | (package cmdlang) 5 | (glob_files *.txt) 6 | (glob_files *.exe) 7 | (glob_files *.mli) 8 | (glob_files *.ml)) 9 | (preludes prelude.txt)) 10 | 11 | (rule 12 | (copy ./lib/usage_styles.mli ./usage_styles.mli)) 13 | 14 | (rule 15 | (copy ./lib/usage_styles.ml ./usage_styles.ml)) 16 | -------------------------------------------------------------------------------- /doc/docs/guides/usage-styles/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name usage_styles) 3 | (public_name cmdlang-tests.usage_styles) 4 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a -open Cmdlang) 5 | (libraries cmdlang) 6 | (lint 7 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 8 | (preprocess 9 | (pps 10 | -unused-code-warnings=force 11 | ppx_compare 12 | ppx_enumerate 13 | ppx_expect 14 | ppx_hash 15 | ppx_here 16 | ppx_let 17 | ppx_sexp_conv 18 | ppx_sexp_value))) 19 | -------------------------------------------------------------------------------- /doc/docs/guides/usage-styles/lib/usage_styles.ml: -------------------------------------------------------------------------------- 1 | (* $MDX part-begin=let_plus_std *) 2 | let _ : unit Command.t = 3 | Command.make 4 | ~summary:"A command skeleton" 5 | (let open Command.Std in 6 | let+ (_ : int) = Arg.named [ "n" ] Param.int ~doc:"A value for n" 7 | and+ () = Arg.return () in 8 | ()) 9 | ;; 10 | 11 | (* $MDX part-end *) 12 | 13 | (* $MDX part-begin=let_plus_std_no_indent *) 14 | let _ : unit Command.t = 15 | Command.make ~summary:"A command skeleton" 16 | @@ 17 | let open Command.Std in 18 | let+ (_ : int) = Arg.named [ "n" ] Param.int ~doc:"A value for n" 19 | and+ () = Arg.return () in 20 | () 21 | ;; 22 | 23 | (* $MDX part-end *) 24 | 25 | (* $MDX part-begin=let_map_open *) 26 | let _ : unit Command.t = 27 | Command.make 28 | ~summary:"A command skeleton" 29 | (let%map_open.Command () = Arg.return () 30 | and () = Arg.return () in 31 | ()) 32 | ;; 33 | (* $MDX part-end *) 34 | -------------------------------------------------------------------------------- /doc/docs/guides/usage-styles/lib/usage_styles.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/doc/docs/guides/usage-styles/lib/usage_styles.mli -------------------------------------------------------------------------------- /doc/docs/guides/usage-styles/prelude.txt: -------------------------------------------------------------------------------- 1 | #require "cmdlang" ;; 2 | -------------------------------------------------------------------------------- /doc/docs/reference/dune: -------------------------------------------------------------------------------- 1 | (mdx 2 | (package cmdlang-tests) 3 | (deps 4 | (package cmdlang) 5 | (glob_files *.txt)) 6 | (preludes prelude.txt)) 7 | -------------------------------------------------------------------------------- /doc/docs/reference/odoc.md: -------------------------------------------------------------------------------- 1 | # OCaml Packages Documentation 2 | 3 | You can view the published odoc pages here: [https://mbarbin.github.io/cmdlang/odoc/](https://mbarbin.github.io/cmdlang/odoc/). 4 | 5 | ## Release status 6 | 7 | We have published an initial release of the following packages to allow interested parties to experiment with cmdlang and provide early feedback. 8 | 9 | | package | released to opam | 10 | |-----------------------|:----------------:| 11 | | cmdlang | ✓ | 12 | | cmdlang-stdlib-runner | ✓ | 13 | | cmdlang-to-base | ✓ | 14 | | cmdlang-to-climate | ✓ | 15 | | cmdlang-to-cmdliner | ✓ | 16 | -------------------------------------------------------------------------------- /doc/docs/reference/prelude.txt: -------------------------------------------------------------------------------- 1 | #require "cmdlang" ;; 2 | -------------------------------------------------------------------------------- /doc/docs/tutorials/getting-started/bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 4 | (libraries cmdlang-to-cmdliner cmdliner getting_started) 5 | (instrumentation 6 | (backend bisect_ppx))) 7 | -------------------------------------------------------------------------------- /doc/docs/tutorials/getting-started/bin/main.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | let code = 3 | Cmdliner.Cmd.eval 4 | (Cmdlang_to_cmdliner.Translate.command 5 | Getting_started.cmd 6 | ~name:"my-calculator" 7 | ~version:"%%VERSION%%") 8 | in 9 | (* We disable coverage here because [bisect_ppx] instruments the out-edge of 10 | calls to [exit], which never returns. This creates false negatives in test 11 | coverage. We may revisit this decision in the future if the context 12 | changes. *) 13 | (exit code [@coverage off]) 14 | ;; 15 | -------------------------------------------------------------------------------- /doc/docs/tutorials/getting-started/dune: -------------------------------------------------------------------------------- 1 | (mdx 2 | (package cmdlang-tests) 3 | (deps 4 | (package cmdlang) 5 | my-calculator 6 | (glob_files *.txt) 7 | (glob_files *.exe) 8 | (glob_files *.mli) 9 | (glob_files *.ml)) 10 | (preludes prelude.txt)) 11 | 12 | (rule 13 | (copy ./bin/main.ml ./main.ml)) 14 | 15 | (rule 16 | (copy ./lib/getting_started.mli ./getting_started.mli)) 17 | 18 | (rule 19 | (copy ./lib/getting_started.ml ./getting_started.ml)) 20 | 21 | (rule 22 | (copy ./bin/main.exe ./my-calculator)) 23 | -------------------------------------------------------------------------------- /doc/docs/tutorials/getting-started/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name getting_started) 3 | (public_name cmdlang-tests.getting_started) 4 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a -open Cmdlang) 5 | (libraries cmdlang) 6 | (instrumentation 7 | (backend bisect_ppx))) 8 | -------------------------------------------------------------------------------- /doc/docs/tutorials/getting-started/lib/getting_started.ml: -------------------------------------------------------------------------------- 1 | (* $MDX part-begin=start *) 2 | module Operator = struct 3 | type t = 4 | | Add 5 | | Mul 6 | 7 | let all = [ Add; Mul ] 8 | 9 | let to_string = function 10 | | Add -> "add" 11 | | Mul -> "mul" 12 | ;; 13 | 14 | let eval op a b = 15 | match op with 16 | | Add -> a +. b 17 | | Mul -> a *. b 18 | ;; 19 | end 20 | (* $MDX part-end *) 21 | 22 | (* $MDX part-begin=final *) 23 | let cmd = 24 | Command.make 25 | ~summary:"A simple calculator" 26 | (let open Command.Std in 27 | let+ op = 28 | Arg.named 29 | [ "op" ] 30 | (Param.enumerated (module Operator)) 31 | ~docv:"OP" 32 | ~doc:"operation to perform" 33 | and+ a = Arg.pos ~pos:0 Param.float ~docv:"a" ~doc:"first operand" 34 | and+ b = Arg.pos ~pos:1 Param.float ~docv:"b" ~doc:"second operand" 35 | and+ verbose = Arg.flag [ "verbose" ] ~doc:"print debug information" in 36 | if verbose then Printf.printf "op: %s, a: %f, b: %f\n" (Operator.to_string op) a b; 37 | print_endline (Operator.eval op a b |> string_of_float)) 38 | ;; 39 | (* $MDX part-end *) 40 | -------------------------------------------------------------------------------- /doc/docs/tutorials/getting-started/lib/getting_started.mli: -------------------------------------------------------------------------------- 1 | (* $MDX part-begin=export *) 2 | val cmd : unit Command.t 3 | (* $MDX part-end *) 4 | -------------------------------------------------------------------------------- /doc/docs/tutorials/getting-started/prelude.txt: -------------------------------------------------------------------------------- 1 | #require "cmdlang" ;; 2 | -------------------------------------------------------------------------------- /doc/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import { themes as prismThemes } from 'prism-react-renderer'; 2 | import type { Config } from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | 5 | const config: Config = { 6 | title: 'cmdlang', 7 | tagline: 'Declarative Command-line Parsing for OCaml', 8 | favicon: 'img/favicon.ico', 9 | 10 | // Set the production url of your site here 11 | url: 'https://mbarbin.github.io', 12 | // Set the // pathname under which your site is served 13 | // For GitHub pages deployment, it is often '//' 14 | baseUrl: '/cmdlang/', 15 | 16 | // GitHub pages deployment config. 17 | // If you aren't using GitHub pages, you don't need these. 18 | organizationName: 'mbarbin', // Usually your GitHub org/user name. 19 | projectName: 'cmdlang', // Usually your repo name. 20 | 21 | trailingSlash: true, 22 | 23 | onBrokenLinks: 'throw', 24 | onBrokenMarkdownLinks: 'warn', 25 | 26 | // Even if you don't use internationalization, you can use this field to set 27 | // useful metadata like html lang. For example, if your site is Chinese, you 28 | // may want to replace "en" with "zh-Hans". 29 | i18n: { 30 | defaultLocale: 'en', 31 | locales: ['en'], 32 | }, 33 | 34 | presets: [ 35 | [ 36 | 'classic', 37 | { 38 | docs: { 39 | sidebarPath: './sidebars.ts', 40 | // Please change this to your repo. 41 | // Remove this to remove the "edit this page" links. 42 | editUrl: 'https://github.com/mbarbin/cmdlang/tree/main/doc/', 43 | }, 44 | blog: { 45 | showReadingTime: true, 46 | // Please change this to your repo. 47 | // Remove this to remove the "edit this page" links. 48 | editUrl: 'https://github.com/mbarbin/cmdlang/tree/main/doc/', 49 | }, 50 | theme: { 51 | customCss: './src/css/custom.css', 52 | }, 53 | } satisfies Preset.Options, 54 | ], 55 | ], 56 | 57 | markdown: { 58 | mermaid: true, 59 | }, 60 | 61 | themes: ['@docusaurus/theme-mermaid'], 62 | 63 | themeConfig: { 64 | // Replace with your project's social card 65 | image: 'img/cmdlang.jpg', 66 | navbar: { 67 | hideOnScroll: true, 68 | title: 'cmdlang', 69 | logo: { 70 | alt: 'Site Logo', 71 | src: 'img/cmdlang.jpg', 72 | }, 73 | items: [ 74 | { 75 | type: 'docSidebar', 76 | sidebarId: 'tutorialsSidebar', 77 | position: 'left', 78 | label: 'Tutorials', 79 | }, 80 | { 81 | type: 'docSidebar', 82 | sidebarId: 'guidesSidebar', 83 | position: 'left', 84 | label: 'Guides', 85 | }, 86 | { 87 | type: 'docSidebar', 88 | sidebarId: 'referenceSidebar', 89 | position: 'left', 90 | label: 'Reference', 91 | }, 92 | { 93 | type: 'docSidebar', 94 | sidebarId: 'explanationSidebar', 95 | position: 'left', 96 | label: 'Explanation', 97 | }, 98 | { to: '/blog/', label: 'Blog', position: 'right' }, 99 | { 100 | href: 'https://github.com/mbarbin/cmdlang', 101 | label: 'GitHub', 102 | position: 'right', 103 | }, 104 | ], 105 | }, 106 | docs: { 107 | sidebar: { 108 | hideable: true, 109 | autoCollapseCategories: true, 110 | }, 111 | }, 112 | footer: { 113 | style: 'dark', 114 | links: [ 115 | { 116 | title: 'Docs', 117 | items: [ 118 | { 119 | label: 'Tutorials', 120 | to: '/docs/tutorials/getting-started/', 121 | }, 122 | { 123 | label: 'Guides', 124 | to: '/docs/guides/usage-styles/', 125 | }, 126 | { 127 | label: 'Reference', 128 | to: '/docs/reference/odoc/', 129 | }, 130 | { 131 | label: 'Explanation', 132 | to: '/docs/explanation/', 133 | }, 134 | ], 135 | }, 136 | { 137 | title: 'More', 138 | items: [ 139 | { 140 | label: 'Blog', 141 | to: '/blog', 142 | }, 143 | { 144 | label: 'GitHub', 145 | href: 'https://github.com/mbarbin/cmdlang', 146 | }, 147 | ], 148 | }, 149 | ], 150 | copyright: `Copyright © ${new Date().getFullYear()} Mathieu Barbin. Built with Docusaurus.`, 151 | }, 152 | prism: { 153 | theme: prismThemes.github, 154 | darkTheme: prismThemes.dracula, 155 | additionalLanguages: ['bash', 'diff', 'json', 'ocaml'], 156 | }, 157 | } satisfies Preset.ThemeConfig, 158 | }; 159 | 160 | export default config; 161 | -------------------------------------------------------------------------------- /doc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmdlang", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "~3.5.2", 19 | "@docusaurus/preset-classic": "~3.5.2", 20 | "@docusaurus/theme-mermaid": "~3.5.2", 21 | "@mdx-js/react": "~3.0.1", 22 | "clsx": "~2.1.1", 23 | "prism-react-renderer": "~2.4.0", 24 | "react": "~18.3.1", 25 | "react-dom": "~18.3.1" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "~3.5.2", 29 | "@docusaurus/tsconfig": "~3.5.2", 30 | "@docusaurus/types": "~3.5.2", 31 | "@types/react": "~18.3.8", 32 | "typescript": "~5.6.2" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 3 chrome version", 42 | "last 3 firefox version", 43 | "last 5 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=18.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /doc/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | 15 | tutorialsSidebar: [ 16 | { 17 | type: 'category', 18 | label: 'Tutorials', 19 | items: [ 20 | { type: 'doc', id: 'tutorials/getting-started/README', label: 'Getting Started' }, 21 | ], 22 | }, 23 | ], 24 | 25 | guidesSidebar: [ 26 | { 27 | type: 'category', 28 | label: 'Guides', 29 | items: [ 30 | { type: 'doc', id: 'guides/usage-styles/README', label: 'Usage Styles' }, 31 | ], 32 | }, 33 | ], 34 | 35 | referenceSidebar: [ 36 | { 37 | type: 'category', 38 | label: 'Reference', 39 | items: [ 40 | { type: 'doc', id: 'reference/odoc', label: 'OCaml Packages' }, 41 | ], 42 | }, 43 | ], 44 | 45 | explanationSidebar: [ 46 | { 47 | type: 'category', 48 | label: 'Explanation', 49 | items: [ 50 | { type: 'doc', id: 'explanation/README', label: 'Introduction' }, 51 | { type: 'doc', id: 'explanation/future_plans', label: 'Future Plans' }, 52 | { type: 'doc', id: 'explanation/faq', label: 'Frequently Asked Questions' }, 53 | ], 54 | }, 55 | ], 56 | }; 57 | 58 | export default sidebars; 59 | -------------------------------------------------------------------------------- /doc/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /doc/src/pages/index.md: -------------------------------------------------------------------------------- 1 |

2 |

Declarative Command-line Parsing for OCaml

3 | Logo 8 |

9 | 10 |

11 | CI Status 12 | Coverage Status 13 | Deploy Doc Status 14 |

15 | 16 | Cmdlang is a library for creating command-line parsers in OCaml. Implemented as an OCaml EDSL, its declarative specification language lives at the intersection of other well-established similar libraries. 17 | 18 | Cmdlang doesn't include an execution engine. Instead, Cmdlang parsers are automatically translated to `cmdliner`, `core.command`, or `climate` commands for execution. 19 | 20 | Our goal is to provide an approachable, flexible, and user-friendly interface while allowing users to choose the backend runtime that best suits their needs. 21 | -------------------------------------------------------------------------------- /doc/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/doc/static/.nojekyll -------------------------------------------------------------------------------- /doc/static/img/cmdlang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/doc/static/img/cmdlang.jpg -------------------------------------------------------------------------------- /doc/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/doc/static/img/favicon.ico -------------------------------------------------------------------------------- /doc/static/odoc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | index 5 | 6 | 7 | 8 | 9 |
10 |
11 |

OCaml package documentation

12 | This directory is meant to be replaced by the odoc documentation during deployment. 13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /doc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (env 2 | (dev 3 | (odoc 4 | (warnings fatal)))) 5 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.17) 2 | 3 | (name cmdlang) 4 | 5 | (generate_opam_files) 6 | 7 | (license MIT) 8 | 9 | (authors "Mathieu Barbin") 10 | 11 | (maintainers "Mathieu Barbin ") 12 | 13 | (source 14 | (github mbarbin/cmdlang)) 15 | 16 | (documentation "https://mbarbin.github.io/cmdlang/") 17 | 18 | (using mdx 0.4) 19 | 20 | ;; The value for the [implicit_transtive_deps] option is set during the CI 21 | ;; depending on the OCaml compiler version. 22 | ;; 23 | ;; This will be set to [false] iif [ocaml-version >= 5.2]. 24 | ;; 25 | ;; For packaging purposes with older ocaml, it is simpler atm if the option is 26 | ;; set to [true] in the main branch. 27 | ;; 28 | ;; See: [.github/workflows/edit_dune_project_dot_ml]. 29 | 30 | (implicit_transitive_deps true) 31 | 32 | (package 33 | (name cmdlang) 34 | (synopsis "Declarative Command-line Parsing for OCaml") 35 | (depends 36 | (ocaml 37 | (>= 4.14)))) 38 | 39 | (package 40 | (name cmdlang-to-base) 41 | (synopsis "Convert cmdlang Parsers to core.command") 42 | (depends 43 | (ocaml 44 | (>= 5.2)) 45 | (base 46 | (>= v0.17)) 47 | (cmdlang 48 | (= :version)) 49 | (core 50 | (>= v0.17)) 51 | (ppx_compare 52 | (>= v0.17)) 53 | (ppx_enumerate 54 | (>= v0.17)) 55 | (ppx_expect 56 | (>= v0.17)) 57 | (ppx_hash 58 | (>= v0.17)) 59 | (ppx_here 60 | (>= v0.17)) 61 | (ppx_let 62 | (>= v0.17)) 63 | (ppx_sexp_conv 64 | (>= v0.17)) 65 | (ppx_sexp_value 66 | (>= v0.17)) 67 | (ppxlib 68 | (>= 0.33)) 69 | (stdio 70 | (>= v0.17)))) 71 | 72 | (package 73 | (name cmdlang-to-cmdliner) 74 | (synopsis "Convert cmdlang Parsers to cmdliner") 75 | (depends 76 | (ocaml 77 | (>= 4.14)) 78 | (cmdlang 79 | (= :version)) 80 | (cmdliner 81 | (>= 1.3.0)))) 82 | 83 | (package 84 | (name cmdlang-to-climate) 85 | (synopsis "Convert cmdlang Parsers to climate") 86 | (depends 87 | (ocaml 88 | (>= 4.14)) 89 | (climate 90 | (>= 0.5.0)) 91 | (cmdlang 92 | (= :version)))) 93 | 94 | (package 95 | (name cmdlang-stdlib-runner) 96 | (synopsis "A basic execution runner for cmdlang based on stdlib.arg") 97 | (depends 98 | (ocaml 99 | (>= 4.14)) 100 | (cmdlang 101 | (= :version)))) 102 | 103 | (package 104 | (name cmdlang-tests) 105 | (synopsis "Tests for cmdlang") 106 | (depends 107 | (ocaml 108 | (>= 5.2)) 109 | (ocamlformat 110 | (and 111 | :with-dev-setup 112 | (= 0.27.0))) 113 | (base 114 | (>= v0.17)) 115 | (bisect_ppx 116 | (and 117 | :with-dev-setup 118 | (>= 2.8.3))) 119 | (climate 120 | (>= 0.5.0)) 121 | (cmdlang 122 | (= :version)) 123 | (cmdlang-stdlib-runner 124 | (= :version)) 125 | (cmdlang-to-base 126 | (= :version)) 127 | (cmdlang-to-climate 128 | (= :version)) 129 | (cmdlang-to-cmdliner 130 | (= :version)) 131 | (cmdliner 132 | (>= 1.3.0)) 133 | (core 134 | (>= v0.17)) 135 | (core_unix 136 | (>= v0.17)) 137 | (expect_test_helpers_core 138 | (>= v0.17)) 139 | (loc 140 | (>= 0.2.2)) 141 | (mdx 142 | (>= 2.4)) 143 | (ppx_compare 144 | (>= v0.17)) 145 | (ppx_enumerate 146 | (>= v0.17)) 147 | (ppx_expect 148 | (>= v0.17)) 149 | (ppx_hash 150 | (>= v0.17)) 151 | (ppx_here 152 | (>= v0.17)) 153 | (ppx_js_style 154 | (and 155 | :with-dev-setup 156 | (>= v0.17))) 157 | (ppx_let 158 | (>= v0.17)) 159 | (ppx_sexp_conv 160 | (>= v0.17)) 161 | (ppx_sexp_value 162 | (>= v0.17)) 163 | (ppxlib 164 | (>= 0.33)) 165 | (stdio 166 | (>= v0.17)) 167 | (stdune 168 | (>= 3.17)) 169 | (sherlodoc 170 | (and 171 | :with-doc 172 | (>= 0.2))))) 173 | -------------------------------------------------------------------------------- /lib/cmdlang/src/command.ml: -------------------------------------------------------------------------------- 1 | module Nonempty_list = struct 2 | type 'a t = 'a Cmdlang_ast.Ast.Nonempty_list.t = ( :: ) : 'a * 'a list -> 'a t 3 | end 4 | 5 | module type Enumerated_stringable = sig 6 | type t 7 | 8 | val all : t list 9 | val to_string : t -> string 10 | end 11 | 12 | module type Stringable = sig 13 | type t 14 | 15 | val of_string : string -> t 16 | val to_string : t -> string 17 | end 18 | 19 | module type Validated_string = sig 20 | type t 21 | 22 | val of_string : string -> (t, [ `Msg of string ]) Result.t 23 | val to_string : t -> string 24 | end 25 | 26 | module Param = struct 27 | type 'a t = 'a Ast.Param.t 28 | type 'a parse = string -> ('a, [ `Msg of string ]) result 29 | type 'a print = Format.formatter -> 'a -> unit 30 | 31 | let create ~docv ~(parse : _ parse) ~(print : _ print) = 32 | Ast.Param.Conv { docv = Some docv; parse; print } 33 | ;; 34 | 35 | let string = Ast.Param.String 36 | let int = Ast.Param.Int 37 | let float = Ast.Param.Float 38 | let bool = Ast.Param.Bool 39 | let file = Ast.Param.File 40 | 41 | let enumerated (type a) ?docv (module M : Enumerated_stringable with type t = a) = 42 | match M.all |> List.map (fun m -> M.to_string m, m) with 43 | | [] -> invalid_arg "Command.Param.enumerated" 44 | | hd :: tl -> Ast.Param.Enum { docv; choices = hd :: tl; to_string = M.to_string } 45 | ;; 46 | 47 | let stringable (type a) ?docv (module M : Stringable with type t = a) = 48 | let parse s = Ok (M.of_string s) 49 | and print ppf x = Format.fprintf ppf "%s" (M.to_string x) in 50 | Ast.Param.Conv { docv; parse; print } 51 | ;; 52 | 53 | let validated_string (type a) ?docv (module M : Validated_string with type t = a) = 54 | let print ppf x = Format.fprintf ppf "%s" (M.to_string x) in 55 | Ast.Param.Conv { docv; parse = M.of_string; print } 56 | ;; 57 | 58 | let comma_separated t = Ast.Param.Comma_separated t 59 | end 60 | 61 | module Arg = struct 62 | type 'a t = 'a Ast.Arg.t 63 | 64 | let return x = Ast.Arg.Return x 65 | let map x ~f = Ast.Arg.Map { x; f } 66 | let both a b = Ast.Arg.Both (a, b) 67 | let ( >>| ) x f = map x ~f 68 | let apply f x = Ast.Arg.Apply { f; x } 69 | let ( let+ ) = ( >>| ) 70 | let ( and+ ) = both 71 | let flag names ~doc = Ast.Arg.Flag { names; doc } 72 | let flag_count names ~doc = Ast.Arg.Flag_count { names; doc } 73 | let named ?docv names param ~doc = Ast.Arg.Named { names; param; docv; doc } 74 | let named_multi ?docv names param ~doc = Ast.Arg.Named_multi { names; param; docv; doc } 75 | let named_opt ?docv names param ~doc = Ast.Arg.Named_opt { names; param; docv; doc } 76 | 77 | let named_with_default ?docv names param ~default ~doc = 78 | Ast.Arg.Named_with_default { names; param; default; docv; doc } 79 | ;; 80 | 81 | let pos ?docv ~pos param ~doc = Ast.Arg.Pos { pos; param; docv; doc } 82 | let pos_opt ?docv ~pos param ~doc = Ast.Arg.Pos_opt { pos; param; docv; doc } 83 | 84 | let pos_with_default ?docv ~pos param ~default ~doc = 85 | Ast.Arg.Pos_with_default { pos; param; default; docv; doc } 86 | ;; 87 | 88 | let pos_all ?docv param ~doc = Ast.Arg.Pos_all { param; docv; doc } 89 | end 90 | 91 | type 'a t = 'a Ast.Command.t 92 | 93 | let make ?readme arg ~summary = Ast.Command.Make { arg; summary; readme } 94 | 95 | let group ?default ?readme ~summary subcommands = 96 | Ast.Command.Group { default; summary; readme; subcommands } 97 | ;; 98 | 99 | module Utils = struct 100 | let summary = Ast.Command.summary 101 | let map = Ast.Command.map 102 | end 103 | 104 | module type Applicative_infix = sig 105 | type 'a t 106 | 107 | val ( >>| ) : 'a t -> ('a -> 'b) -> 'b t 108 | end 109 | 110 | module Applicative_infix : Applicative_infix with type 'a t := 'a Arg.t = struct 111 | open Arg 112 | 113 | let ( >>| ) = ( >>| ) 114 | end 115 | 116 | module type Applicative_syntax = sig 117 | type 'a t 118 | 119 | val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t 120 | val ( and+ ) : 'a t -> 'b t -> ('a * 'b) t 121 | end 122 | 123 | module Applicative_syntax : Applicative_syntax with type 'a t := 'a Arg.t = struct 124 | open Arg 125 | 126 | let ( let+ ) = ( let+ ) 127 | let ( and+ ) = ( and+ ) 128 | end 129 | 130 | module Std = struct 131 | module Arg = Arg 132 | module Param = Param 133 | include Applicative_syntax 134 | include Applicative_infix 135 | end 136 | 137 | module Let_syntax = struct 138 | open Arg 139 | 140 | let return = return 141 | 142 | include Applicative_infix 143 | 144 | module Let_syntax = struct 145 | let return = return 146 | let map = map 147 | let both = both 148 | 149 | module Open_on_rhs = struct 150 | module Arg = Arg 151 | module Param = Param 152 | include Applicative_infix 153 | end 154 | end 155 | end 156 | 157 | module Private = struct 158 | module To_ast = struct 159 | let arg : 'a Arg.t -> 'a Ast.Arg.t = Fun.id 160 | let param : 'a Param.t -> 'a Ast.Param.t = Fun.id 161 | let command : 'a t -> 'a Ast.Command.t = Fun.id 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/cmdlang/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang) 3 | (public_name cmdlang) 4 | (flags 5 | :standard 6 | -w 7 | +a-4-40-41-42-44-45-48-66 8 | -warn-error 9 | +a 10 | -open 11 | Cmdlang_ast) 12 | (libraries cmdlang_ast) 13 | (instrumentation 14 | (backend bisect_ppx)) 15 | (lint 16 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 17 | (preprocess no_preprocessing)) 18 | -------------------------------------------------------------------------------- /lib/cmdlang/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_test) 3 | (public_name cmdlang-tests.cmdlang_test) 4 | (inline_tests) 5 | (flags 6 | :standard 7 | -w 8 | +a-4-40-41-42-44-45-48-66 9 | -warn-error 10 | +a 11 | -open 12 | Base 13 | -open 14 | Expect_test_helpers_base 15 | -open 16 | Cmdlang) 17 | (libraries base cmdlang expect_test_helpers_core.expect_test_helpers_base) 18 | (instrumentation 19 | (backend bisect_ppx)) 20 | (lint 21 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 22 | (preprocess 23 | (pps 24 | -unused-code-warnings=force 25 | ppx_compare 26 | ppx_enumerate 27 | ppx_expect 28 | ppx_hash 29 | ppx_here 30 | ppx_let 31 | ppx_sexp_conv 32 | ppx_sexp_value))) 33 | -------------------------------------------------------------------------------- /lib/cmdlang/test/test__command.ml: -------------------------------------------------------------------------------- 1 | module Empty = struct 2 | type t = | 3 | 4 | let all = [] 5 | 6 | let to_string t = 7 | match[@coverage off] t with 8 | | (_ : t) -> . 9 | ;; 10 | end 11 | 12 | let%expect_test "Param.enumerated" = 13 | let open Command.Std in 14 | require_does_raise [%here] (fun () -> Param.enumerated (module Empty)); 15 | [%expect {| (Invalid_argument Command.Param.enumerated) |}]; 16 | () 17 | ;; 18 | -------------------------------------------------------------------------------- /lib/cmdlang/test/test__command.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang/test/test__command.mli -------------------------------------------------------------------------------- /lib/cmdlang_ast/src/ast.ml: -------------------------------------------------------------------------------- 1 | type 'a or_error_msg = ('a, [ `Msg of string ]) result 2 | type 'a parse = string -> 'a or_error_msg 3 | type 'a print = Format.formatter -> 'a -> unit 4 | 5 | module Nonempty_list = struct 6 | type 'a t = ( :: ) : 'a * 'a list -> 'a t 7 | end 8 | 9 | module Param = struct 10 | type 'a t = 11 | | Conv : 12 | { docv : string option 13 | ; parse : 'a parse 14 | ; print : 'a print 15 | } 16 | -> 'a t 17 | | String : string t 18 | | Int : int t 19 | | Float : float t 20 | | Bool : bool t 21 | | File : string t 22 | | Enum : 23 | { docv : string option 24 | ; choices : (string * 'a) Nonempty_list.t 25 | ; to_string : 'a -> string 26 | } 27 | -> 'a t 28 | | Comma_separated : 'a t -> 'a list t 29 | end 30 | 31 | module Arg = struct 32 | type 'a t = 33 | | Return : 'a -> 'a t 34 | | Map : 35 | { x : 'a t 36 | ; f : 'a -> 'b 37 | } 38 | -> 'b t 39 | | Both : 'a t * 'b t -> ('a * 'b) t 40 | | Apply : 41 | { f : ('a -> 'b) t 42 | ; x : 'a t 43 | } 44 | -> 'b t 45 | | Flag : 46 | { names : string Nonempty_list.t 47 | ; doc : string 48 | } 49 | -> bool t 50 | | Flag_count : 51 | { names : string Nonempty_list.t 52 | ; doc : string 53 | } 54 | -> int t 55 | | Named : 56 | { names : string Nonempty_list.t 57 | ; param : 'a Param.t 58 | ; docv : string option 59 | ; doc : string 60 | } 61 | -> 'a t 62 | | Named_multi : 63 | { names : string Nonempty_list.t 64 | ; param : 'a Param.t 65 | ; docv : string option 66 | ; doc : string 67 | } 68 | -> 'a list t 69 | | Named_opt : 70 | { names : string Nonempty_list.t 71 | ; param : 'a Param.t 72 | ; docv : string option 73 | ; doc : string 74 | } 75 | -> 'a option t 76 | | Named_with_default : 77 | { names : string Nonempty_list.t 78 | ; param : 'a Param.t 79 | ; default : 'a 80 | ; docv : string option 81 | ; doc : string 82 | } 83 | -> 'a t 84 | | Pos : 85 | { pos : int 86 | ; param : 'a Param.t 87 | ; docv : string option 88 | ; doc : string 89 | } 90 | -> 'a t 91 | | Pos_opt : 92 | { pos : int 93 | ; param : 'a Param.t 94 | ; docv : string option 95 | ; doc : string 96 | } 97 | -> 'a option t 98 | | Pos_with_default : 99 | { pos : int 100 | ; param : 'a Param.t 101 | ; default : 'a 102 | ; docv : string option 103 | ; doc : string 104 | } 105 | -> 'a t 106 | | Pos_all : 107 | { param : 'a Param.t 108 | ; docv : string option 109 | ; doc : string 110 | } 111 | -> 'a list t 112 | end 113 | 114 | module Command = struct 115 | type 'a t = 116 | | Make : 117 | { arg : 'a Arg.t 118 | ; summary : string 119 | ; readme : (unit -> string) option 120 | } 121 | -> 'a t 122 | | Group : 123 | { default : 'a Arg.t option 124 | ; summary : string 125 | ; readme : (unit -> string) option 126 | ; subcommands : (string * 'a t) list 127 | } 128 | -> 'a t 129 | 130 | let summary = function 131 | | Make { summary; _ } -> summary 132 | | Group { summary; _ } -> summary 133 | ;; 134 | 135 | let rec map : type a b. a t -> f:(a -> b) -> b t = 136 | fun a ~f -> 137 | match a with 138 | | Make { arg; summary; readme } -> 139 | Make { arg = Arg.Map { x = arg; f }; summary; readme } 140 | | Group { default; summary; readme; subcommands } -> 141 | Group 142 | { default = default |> Option.map (fun arg -> Arg.Map { x = arg; f }) 143 | ; summary 144 | ; readme 145 | ; subcommands = subcommands |> List.map (fun (name, arg) -> name, map arg ~f) 146 | } 147 | ;; 148 | end 149 | -------------------------------------------------------------------------------- /lib/cmdlang_ast/src/ast.mli: -------------------------------------------------------------------------------- 1 | (** The internal representation for the EDSL used by the cmdlang library. 2 | 3 | Cmdlang doesn't include an execution engine. Instead, Cmdlang parsers are 4 | automatically translated to `cmdliner`, `core.command`, or `climate` 5 | commands for execution. 6 | 7 | When you use the cmdlang library to define a command-line interface, you are 8 | effectively building a value of type [Cmdlang_ast.Ast.Command.t]. It is then 9 | converted internally to the targeted backend. 10 | 11 | This module is not meant to be used directly by users of the library. 12 | Rather, users use the [Cmdlang.Command] interface which provides a 13 | high-level API meant to be ergonomic and user-friendly. 14 | 15 | [Cmdlang_ast] is exposed to allow extending the library with new backends or 16 | to write analysis tools, etc. *) 17 | 18 | type 'a or_error_msg = ('a, [ `Msg of string ]) result 19 | type 'a parse := string -> 'a or_error_msg 20 | type 'a print := Format.formatter -> 'a -> unit 21 | 22 | module Nonempty_list : sig 23 | type 'a t = ( :: ) : 'a * 'a list -> 'a t 24 | end 25 | 26 | module Param : sig 27 | type 'a t = 28 | | Conv : 29 | { docv : string option 30 | ; parse : 'a parse 31 | ; print : 'a print 32 | } 33 | -> 'a t 34 | | String : string t 35 | | Int : int t 36 | | Float : float t 37 | | Bool : bool t 38 | | File : string t 39 | | Enum : 40 | { docv : string option 41 | ; choices : (string * 'a) Nonempty_list.t 42 | ; to_string : 'a -> string 43 | } 44 | -> 'a t 45 | | Comma_separated : 'a t -> 'a list t 46 | end 47 | 48 | module Arg : sig 49 | type 'a t = 50 | | Return : 'a -> 'a t 51 | | Map : 52 | { x : 'a t 53 | ; f : 'a -> 'b 54 | } 55 | -> 'b t 56 | | Both : 'a t * 'b t -> ('a * 'b) t 57 | | Apply : 58 | { f : ('a -> 'b) t 59 | ; x : 'a t 60 | } 61 | -> 'b t 62 | | Flag : 63 | { names : string Nonempty_list.t 64 | ; doc : string 65 | } 66 | -> bool t 67 | | Flag_count : 68 | { names : string Nonempty_list.t 69 | ; doc : string 70 | } 71 | -> int t 72 | | Named : 73 | { names : string Nonempty_list.t 74 | ; param : 'a Param.t 75 | ; docv : string option 76 | ; doc : string 77 | } 78 | -> 'a t 79 | | Named_multi : 80 | { names : string Nonempty_list.t 81 | ; param : 'a Param.t 82 | ; docv : string option 83 | ; doc : string 84 | } 85 | -> 'a list t 86 | | Named_opt : 87 | { names : string Nonempty_list.t 88 | ; param : 'a Param.t 89 | ; docv : string option 90 | ; doc : string 91 | } 92 | -> 'a option t 93 | | Named_with_default : 94 | { names : string Nonempty_list.t 95 | ; param : 'a Param.t 96 | ; default : 'a 97 | ; docv : string option 98 | ; doc : string 99 | } 100 | -> 'a t 101 | | Pos : 102 | { pos : int 103 | ; param : 'a Param.t 104 | ; docv : string option 105 | ; doc : string 106 | } 107 | -> 'a t 108 | | Pos_opt : 109 | { pos : int 110 | ; param : 'a Param.t 111 | ; docv : string option 112 | ; doc : string 113 | } 114 | -> 'a option t 115 | | Pos_with_default : 116 | { pos : int 117 | ; param : 'a Param.t 118 | ; default : 'a 119 | ; docv : string option 120 | ; doc : string 121 | } 122 | -> 'a t 123 | | Pos_all : 124 | { param : 'a Param.t 125 | ; docv : string option 126 | ; doc : string 127 | } 128 | -> 'a list t 129 | end 130 | 131 | module Command : sig 132 | type 'a t = 133 | | Make : 134 | { arg : 'a Arg.t 135 | ; summary : string 136 | ; readme : (unit -> string) option 137 | } 138 | -> 'a t 139 | | Group : 140 | { default : 'a Arg.t option 141 | ; summary : string 142 | ; readme : (unit -> string) option 143 | ; subcommands : (string * 'a t) list 144 | } 145 | -> 'a t 146 | 147 | val summary : _ t -> string 148 | val map : 'a t -> f:('a -> 'b) -> 'b t 149 | end 150 | -------------------------------------------------------------------------------- /lib/cmdlang_ast/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_ast) 3 | (public_name cmdlang.ast) 4 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 5 | (instrumentation 6 | (backend bisect_ppx)) 7 | (lint 8 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 9 | (preprocess no_preprocessing)) 10 | -------------------------------------------------------------------------------- /lib/cmdlang_ast/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_ast_test) 3 | (public_name cmdlang-tests.cmdlang_ast_test) 4 | (inline_tests) 5 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 6 | (libraries cmdlang_ast) 7 | (instrumentation 8 | (backend bisect_ppx)) 9 | (lint 10 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 11 | (preprocess 12 | (pps 13 | -unused-code-warnings=force 14 | ppx_compare 15 | ppx_enumerate 16 | ppx_expect 17 | ppx_hash 18 | ppx_here 19 | ppx_let 20 | ppx_sexp_conv 21 | ppx_sexp_value))) 22 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/arg_runner.ml: -------------------------------------------------------------------------------- 1 | type 'a t = 2 | | Value : 'a -> 'a t 3 | | Map : 4 | { x : 'a t 5 | ; f : 'a -> 'b 6 | } 7 | -> 'b t 8 | | Both : 'a t * 'b t -> ('a * 'b) t 9 | | Apply : 10 | { f : ('a -> 'b) t 11 | ; x : 'a t 12 | } 13 | -> 'b t 14 | 15 | let rec eval : type a. a t -> a = 16 | fun (type a) (t : a t) : a -> 17 | match t with 18 | | Value a -> a 19 | | Map { x; f } -> f (eval x) 20 | | Both (a, b) -> 21 | let a = eval a in 22 | let b = eval b in 23 | a, b 24 | | Apply { f; x } -> 25 | let f = eval f in 26 | let x = eval x in 27 | f x 28 | ;; 29 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/arg_runner.mli: -------------------------------------------------------------------------------- 1 | (** Internal representation used to run a parser. 2 | 3 | This is the final representation returned after all of the parsing phases 4 | have completed, and is ready to run user code. *) 5 | 6 | type 'a t = 7 | | Value : 'a -> 'a t 8 | | Map : 9 | { x : 'a t 10 | ; f : 'a -> 'b 11 | } 12 | -> 'b t 13 | | Both : 'a t * 'b t -> ('a * 'b) t 14 | | Apply : 15 | { f : ('a -> 'b) t 16 | ; x : 'a t 17 | } 18 | -> 'b t 19 | 20 | val eval : 'a t -> 'a 21 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/arg_state.mli: -------------------------------------------------------------------------------- 1 | (** Internal representation for cmdlang arg expressions used during parsing. 2 | 3 | This is a projection of [Cmdlang.Ast.Arg.t] where we added mutable variables 4 | to collect and store the intermediate results of parsing the command line 5 | arguments during the parsing phase of the execution. 6 | 7 | To give a concrete example, let's look at the [Flag] construct. In the ast, 8 | the type is: 9 | 10 | {[ 11 | | Flag : 12 | { names : string Ast.Nonempty_list.t 13 | ; doc : string 14 | } 15 | -> bool t 16 | ]} 17 | 18 | Note how, in this intermediate representation we added a new mutable field 19 | as a place to collect and store the value for that flag: [var : bool ref]: 20 | 21 | {[ 22 | | Flag : 23 | { names : string Ast.Nonempty_list.t 24 | ; doc : string 25 | ; var : bool ref (* <== Added mutable field *) 26 | } 27 | -> bool t 28 | ]} 29 | 30 | This [var] is where the parsing engine will store the value read from the 31 | command line. Then the rest of the execution chain will be able to read the 32 | value from there while going through this runtime ast for evaluation, after 33 | the parsing is complete. *) 34 | 35 | type 'a t = 36 | | Return : 'a -> 'a t 37 | | Map : 38 | { x : 'a t 39 | ; f : 'a -> 'b 40 | } 41 | -> 'b t 42 | | Both : 'a t * 'b t -> ('a * 'b) t 43 | | Apply : 44 | { f : ('a -> 'b) t 45 | ; x : 'a t 46 | } 47 | -> 'b t 48 | | Flag : 49 | { names : string Ast.Nonempty_list.t 50 | ; doc : string 51 | ; var : bool ref 52 | } 53 | -> bool t 54 | | Flag_count : 55 | { names : string Ast.Nonempty_list.t 56 | ; doc : string 57 | ; var : int ref 58 | } 59 | -> int t 60 | | Named : 61 | { names : string Ast.Nonempty_list.t 62 | ; param : 'a Ast.Param.t 63 | ; docv : string option 64 | ; doc : string 65 | ; var : 'a option ref 66 | } 67 | -> 'a t 68 | | Named_multi : 69 | { names : string Ast.Nonempty_list.t 70 | ; param : 'a Ast.Param.t 71 | ; docv : string option 72 | ; doc : string 73 | ; rev_var : 'a list ref 74 | } 75 | -> 'a list t 76 | | Named_opt : 77 | { names : string Ast.Nonempty_list.t 78 | ; param : 'a Ast.Param.t 79 | ; docv : string option 80 | ; doc : string 81 | ; var : 'a option ref 82 | } 83 | -> 'a option t 84 | | Named_with_default : 85 | { names : string Ast.Nonempty_list.t 86 | ; param : 'a Ast.Param.t 87 | ; default : 'a 88 | ; docv : string option 89 | ; doc : string 90 | ; var : 'a option ref 91 | } 92 | -> 'a t 93 | | Pos : 94 | { pos : int 95 | ; param : 'a Ast.Param.t 96 | ; docv : string option 97 | ; doc : string 98 | ; var : 'a option ref 99 | } 100 | -> 'a t 101 | | Pos_opt : 102 | { pos : int 103 | ; param : 'a Ast.Param.t 104 | ; docv : string option 105 | ; doc : string 106 | ; var : 'a option ref 107 | } 108 | -> 'a option t 109 | | Pos_with_default : 110 | { pos : int 111 | ; param : 'a Ast.Param.t 112 | ; default : 'a 113 | ; docv : string option 114 | ; doc : string 115 | ; var : 'a option ref 116 | } 117 | -> 'a t 118 | | Pos_all : 119 | { param : 'a Ast.Param.t 120 | ; docv : string option 121 | ; doc : string 122 | ; rev_var : 'a list ref 123 | } 124 | -> 'a list t 125 | 126 | (** Recursively allocate an arg state for all arguments contained in a parser. *) 127 | val create : 'a Ast.Arg.t -> 'a t 128 | 129 | (** {1 Finalization} 130 | 131 | This part of the interface deals with finalizing the state and returning an 132 | expression suitable for execution. 133 | 134 | It must be called last, once all the parsing and mutating is done. *) 135 | 136 | module Parse_error : sig 137 | type t = 138 | | Missing_argument : 139 | { names : string Ast.Nonempty_list.t 140 | ; param : 'a Ast.Param.t 141 | ; docv : string option 142 | ; doc : string 143 | } 144 | -> t 145 | | Missing_positional_argument : 146 | { pos : int 147 | ; param : 'a Ast.Param.t 148 | ; docv : string option 149 | ; doc : string 150 | } 151 | -> t 152 | end 153 | 154 | (** The idea with [finalize] is to split the execution into 2 isolated parts : 155 | the part where the command line is parsed, and the part where user code is 156 | actually ran. *) 157 | val finalize : 'a t -> ('a Arg_runner.t, Parse_error.t) Result.t 158 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/cmdlang_stdlib_runner.ml: -------------------------------------------------------------------------------- 1 | module Arg_runner = Arg_runner 2 | module Arg_state = Arg_state 3 | module Command_selector = Command_selector 4 | module Param_parser = Param_parser 5 | module Parser_state = Parser_state 6 | module Positional_state = Positional_state 7 | 8 | let usage_msg 9 | ~argv 10 | ~resume_parsing_from_index 11 | ~summary 12 | ~readme 13 | ~subcommands 14 | ~positional_state 15 | = 16 | let usage_prefix = 17 | Array.sub argv 0 resume_parsing_from_index |> Array.to_list |> String.concat " " 18 | in 19 | let subcommands = 20 | match subcommands with 21 | | [] -> "" 22 | | _ :: _ as subcommands -> 23 | let subcommands = 24 | subcommands 25 | |> List.map (fun (name, command) -> 26 | let summary = Ast.Command.summary command in 27 | name, summary) 28 | in 29 | let padding = 30 | List.fold_left (fun acc (name, _) -> max acc (String.length name)) 0 subcommands 31 | + 2 32 | in 33 | let items = 34 | subcommands 35 | |> List.map (fun (name, summary) -> 36 | Printf.sprintf " %-*s %s" padding name summary) 37 | |> String.concat "\n" 38 | in 39 | "Subcommands:\n" ^ items ^ "\n\n" 40 | in 41 | let positional_suffix, positional_state = 42 | match 43 | match positional_state with 44 | | None -> None 45 | | Some positional_state -> Positional_state.usage_msg positional_state 46 | with 47 | | None -> "", "" 48 | | Some msg -> " [ARGUMENTS]", msg ^ "\n\n" 49 | in 50 | Printf.sprintf 51 | "Usage: %s [OPTIONS]%s\n\n%s\n\n%s%s%sOptions:" 52 | usage_prefix 53 | positional_suffix 54 | summary 55 | (match readme with 56 | | None -> "" 57 | | Some m -> m () ^ "\n\n") 58 | subcommands 59 | positional_state 60 | ;; 61 | 62 | let eval_arg 63 | (type a) 64 | ~(arg : a Ast.Arg.t) 65 | ~summary 66 | ~readme 67 | ~subcommands 68 | ~argv 69 | ~resume_parsing_from_index 70 | = 71 | let state = 72 | match Parser_state.create arg with 73 | | Ok state -> state 74 | | Error (`Msg msg) -> 75 | let message = "Invalid command specification (programming error):\n\n" ^ msg in 76 | raise (Arg.Bad message) 77 | in 78 | let spec = Parser_state.spec state |> Arg.align in 79 | let positional_state = Parser_state.positional_state state in 80 | let anon_fun = Positional_state.anon_fun positional_state in 81 | let usage_msg ~readme = 82 | usage_msg 83 | ~argv 84 | ~resume_parsing_from_index 85 | ~summary 86 | ~readme 87 | ~subcommands 88 | ~positional_state:(Some positional_state) 89 | in 90 | let () = 91 | let current = ref (resume_parsing_from_index - 1) in 92 | try Arg.parse_argv ~current argv spec anon_fun (usage_msg ~readme:None) with 93 | | Arg.Help _ -> 94 | (* We rewrite the help in order to add the [readme] section. We do not 95 | want to add it by default in the [Arg.Bad] case. *) 96 | let message = Arg.usage_string spec (usage_msg ~readme) in 97 | raise (Arg.Help message) 98 | in 99 | match Parser_state.finalize state with 100 | | Ok runner -> Arg_runner.eval runner 101 | | Error parse_error -> 102 | (match parse_error with 103 | | Arg_state.Parse_error.Missing_argument 104 | { names = name :: _; param = _; docv = _; doc = _ } -> 105 | raise (Arg.Bad (Printf.sprintf "Missing required named argument: %S.\n" name)) 106 | | Arg_state.Parse_error.Missing_positional_argument 107 | { pos; param = _; docv = _; doc = _ } -> 108 | raise 109 | (Arg.Bad 110 | (Printf.sprintf "Missing required positional argument at position %d.\n" pos))) 111 | ;; 112 | 113 | let eval_internal (type a) (command : a Ast.Command.t) ~argv = 114 | let { Command_selector.Selected.command; resume_parsing_from_index } = 115 | Command_selector.select command ~argv 116 | in 117 | match command with 118 | | Make { arg; summary; readme } -> 119 | eval_arg ~arg ~summary ~readme ~subcommands:[] ~argv ~resume_parsing_from_index 120 | | Group { default; summary; readme; subcommands } -> 121 | (match default with 122 | | Some arg -> 123 | eval_arg ~arg ~summary ~readme ~subcommands ~argv ~resume_parsing_from_index 124 | | None -> 125 | let message = 126 | usage_msg 127 | ~argv 128 | ~resume_parsing_from_index 129 | ~summary 130 | ~readme 131 | ~subcommands 132 | ~positional_state:None 133 | in 134 | let arg = 135 | let message = Arg.usage_string [] message in 136 | Ast.Arg.(Map { x = Return (); f = (fun () -> raise (Arg.Bad message)) }) 137 | in 138 | eval_arg ~arg ~summary ~readme ~subcommands ~argv ~resume_parsing_from_index) 139 | ;; 140 | 141 | module To_ast = Cmdlang.Command.Private.To_ast 142 | 143 | let eval a ~argv = 144 | let command = a |> To_ast.command in 145 | try Ok (eval_internal command ~argv) with 146 | | Arg.Help msg -> Error (`Help msg) 147 | | Arg.Bad msg -> Error (`Bad msg) 148 | ;; 149 | 150 | let eval_exit_code a ~argv = 151 | match eval a ~argv with 152 | | Ok () -> 0 153 | | Error (`Bad msg) -> 154 | Printf.printf "%s" msg; 155 | 2 156 | | Error (`Help msg) -> 157 | Printf.printf "%s" msg; 158 | 0 159 | ;; 160 | 161 | let run a = 162 | match eval a ~argv:Sys.argv with 163 | | Ok a -> a 164 | | Error (`Bad msg) -> 165 | Printf.printf "%s" msg; 166 | exit 2 167 | | Error (`Help msg) -> 168 | Printf.printf "%s" msg; 169 | exit 0 170 | ;; 171 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/cmdlang_stdlib_runner.mli: -------------------------------------------------------------------------------- 1 | (** An execution engine for [cmdlang] based on [stdlib.arg]. *) 2 | 3 | val run : 'a Cmdlang.Command.t -> 'a 4 | 5 | val eval 6 | : 'a Cmdlang.Command.t 7 | -> argv:string array 8 | -> ('a, [ `Help of string | `Bad of string ]) Result.t 9 | 10 | val eval_exit_code : unit Cmdlang.Command.t -> argv:string array -> int 11 | 12 | (** {1 Low level implementation} 13 | 14 | This modules should not be used directly by the users of the runner, but 15 | only through the {!run} and {!eval} functions. They are exposed if you want 16 | to re-use some existing code to build your own runner. *) 17 | 18 | module Arg_runner = Arg_runner 19 | module Arg_state = Arg_state 20 | module Command_selector = Command_selector 21 | module Param_parser = Param_parser 22 | module Parser_state = Parser_state 23 | module Positional_state = Positional_state 24 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/command_selector.ml: -------------------------------------------------------------------------------- 1 | module Selected = struct 2 | type 'a t = 3 | { command : 'a Ast.Command.t 4 | ; resume_parsing_from_index : int 5 | } 6 | end 7 | 8 | let select (type a) command ~argv = 9 | let rec aux index command = 10 | match (command : a Ast.Command.t) with 11 | | Make _ -> { Selected.command; resume_parsing_from_index = index } 12 | | Group { default = _; summary = _; readme = _; subcommands } -> 13 | if index >= Array.length argv 14 | then { Selected.command; resume_parsing_from_index = index } 15 | else ( 16 | let arg = argv.(index) in 17 | match subcommands |> List.find_opt (fun (name, _) -> String.equal arg name) with 18 | | Some (_, subcommand) -> aux (index + 1) subcommand 19 | | None -> { Selected.command; resume_parsing_from_index = index }) 20 | in 21 | aux 1 command 22 | ;; 23 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/command_selector.mli: -------------------------------------------------------------------------------- 1 | (** Selecting a command within a group hierarchy. 2 | 3 | Cmdlang supports grouping subcommands into a nested tree, whereas 4 | [stdlib.arg] works at the level of a command leaves. This module is used to 5 | navigate the command tree to select the one based on the prefix of the 6 | command line. 7 | 8 | For example, given the following command invocation: 9 | 10 | {[ 11 | ./my_command group1 subcommand --flag value 12 | ]} 13 | 14 | this module will select from the command tree the subcommand named 15 | [subcommand] from the group [group1]. It will also return the index at which 16 | the parsing should resume, in this case [3] (the index of [--flag] in 17 | [Sys.argv]). *) 18 | 19 | module Selected : sig 20 | type 'a t = 21 | { command : 'a Ast.Command.t 22 | ; resume_parsing_from_index : int 23 | } 24 | end 25 | 26 | val select : 'a Ast.Command.t -> argv:string array -> 'a Selected.t 27 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_stdlib_runner) 3 | (public_name cmdlang-stdlib-runner) 4 | (flags 5 | :standard 6 | -w 7 | +a-4-40-41-42-44-45-48-66 8 | -warn-error 9 | +a 10 | -open 11 | Cmdlang_ast) 12 | (libraries cmdlang cmdlang_ast) 13 | (instrumentation 14 | (backend bisect_ppx)) 15 | (lint 16 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 17 | (preprocess no_preprocessing)) 18 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/import.ml: -------------------------------------------------------------------------------- 1 | module Array = struct 2 | include Array 3 | 4 | (* [Array.find_mapi] available only since 5.1 *) 5 | let find_mapi f a = 6 | let n = length a in 7 | let rec loop i = 8 | if i = n 9 | then None 10 | else ( 11 | match f i (unsafe_get a i) with 12 | | None -> loop (succ i) 13 | | Some _ as r -> r) 14 | in 15 | loop 0 16 | ;; 17 | end 18 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/import.mli: -------------------------------------------------------------------------------- 1 | module Array : sig 2 | include module type of Array 3 | 4 | val find_mapi : (int -> 'a -> 'b option) -> 'a array -> 'b option 5 | end 6 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/param_parser.ml: -------------------------------------------------------------------------------- 1 | let rec eval : type a. a Ast.Param.t -> string -> a Ast.or_error_msg = 2 | fun (type a) (param : a Ast.Param.t) (str : string) : a Ast.or_error_msg -> 3 | let err msg = Error (`Msg msg) in 4 | match param with 5 | | Conv { docv = _; parse; print = _ } -> parse str 6 | | String -> Ok str 7 | | Int -> 8 | (match int_of_string_opt str with 9 | | Some a -> Ok a 10 | | None -> err (Printf.sprintf "invalid value %S (not an int)" str)) 11 | | Float -> 12 | (match float_of_string_opt str with 13 | | Some a -> Ok a 14 | | None -> err (Printf.sprintf "invalid value %S (not a float)" str)) 15 | | Bool -> 16 | (match bool_of_string_opt str with 17 | | Some a -> Ok a 18 | | None -> err (Printf.sprintf "invalid value %S (not a bool)" str)) 19 | | File -> Ok str 20 | | Enum { docv = _; choices = hd :: tl; to_string = _ } -> 21 | (match hd :: tl |> List.find_opt (fun (choice, _) -> String.equal choice str) with 22 | | Some (_, a) -> Ok a 23 | | None -> err (Printf.sprintf "invalid value %S (not a valid choice)" str)) 24 | | Comma_separated param -> 25 | let params = String.split_on_char ',' str in 26 | let oks, errors = 27 | params 28 | |> List.partition_map (fun str -> 29 | match eval param str with 30 | | Ok a -> Either.Left a 31 | | Error (`Msg m) -> Either.Right m) 32 | in 33 | (match errors with 34 | | [] -> Ok oks 35 | | _ :: _ as msgs -> err (String.concat ", " msgs)) 36 | ;; 37 | 38 | let docv : type a. a Ast.Param.t -> docv:string option -> string = 39 | fun param ~docv -> 40 | let rec aux : type a. a Ast.Param.t -> docv:string option -> string = 41 | fun (type a) (param : a Ast.Param.t) ~docv -> 42 | match docv with 43 | | Some v -> v 44 | | None -> 45 | let or_val = function 46 | | Some v -> v 47 | | None -> "VAL" 48 | in 49 | (match param with 50 | | Conv { docv; parse = _; print = _ } -> or_val docv 51 | | String -> "STRING" 52 | | Int -> "INT" 53 | | Float -> "FLOAT" 54 | | Bool -> "BOOL" 55 | | File -> "FILE" 56 | | Enum { docv; choices = _; to_string = _ } -> or_val docv 57 | | Comma_separated param -> aux param ~docv:None) 58 | in 59 | aux param ~docv 60 | ;; 61 | 62 | let rec print : type a. a Ast.Param.t -> a -> string = 63 | fun (type a) (param : a Ast.Param.t) (a : a) : string -> 64 | match param with 65 | | Conv { docv = _; parse = _; print } -> Format.asprintf "%a" print a 66 | | String -> a 67 | | Int -> string_of_int a 68 | | Float -> string_of_float a 69 | | Bool -> string_of_bool a 70 | | File -> a 71 | | Enum { docv = _; choices = hd :: tl; to_string } -> 72 | (match hd :: tl |> List.find_opt (fun (_, b) -> a == b) with 73 | | Some (s, _) -> s 74 | | None -> to_string a) 75 | | Comma_separated param -> a |> List.map (fun a -> print param a) |> String.concat ", " 76 | ;; 77 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/param_parser.mli: -------------------------------------------------------------------------------- 1 | (** Parsing parameters according to their specification. 2 | 3 | This is a util module to convert string based parameters coming from the 4 | command line into their typed representation. 5 | 6 | For example, if a param is expected to be an integer, this module will 7 | convert the string representation of the integer into an actual integer. 8 | 9 | {[ 10 | ./my_command.exe --int-param 42 11 | ]} 12 | 13 | The string ["42"] will be converted into the integer [42], given the 14 | parameter [Ast.Param.Int] for the arg [--int-param]. *) 15 | 16 | val eval : 'a Ast.Param.t -> string -> 'a Ast.or_error_msg 17 | 18 | (** Choose a docv for the help. *) 19 | val docv : _ Ast.Param.t -> docv:string option -> string 20 | 21 | (** Print a param for the help (e.g. document a default value). *) 22 | val print : 'a Ast.Param.t -> 'a -> string 23 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/parser_state.ml: -------------------------------------------------------------------------------- 1 | let make_arg_spec 2 | : type a. name:string -> a Ast.Param.t -> with_var:(a -> unit) -> Arg.spec 3 | = 4 | fun ~name param ~with_var -> 5 | let unspecialized : type a. a Ast.Param.t -> with_var:(a -> unit) -> Arg.spec = 6 | fun param ~with_var -> 7 | Arg.String 8 | (fun s -> 9 | match Param_parser.eval param s with 10 | | Ok v -> with_var v 11 | | Error (`Msg m) -> 12 | raise 13 | (Arg.Bad (Printf.sprintf "Failed to parse the named argument %S: %s" name m))) 14 | in 15 | match param with 16 | | String -> Arg.String with_var 17 | | Int -> Arg.Int with_var 18 | | Float -> Arg.Float with_var 19 | | Bool -> Arg.Bool with_var 20 | | File -> Arg.String with_var 21 | | Enum { docv = _; choices = hd :: tl; to_string = _ } -> 22 | let choices = hd :: tl in 23 | let symbols = List.map fst choices in 24 | Arg.Symbol 25 | ( symbols 26 | , fun symbol -> 27 | choices 28 | |> List.find (fun (choice, _) -> String.equal choice symbol) 29 | |> snd 30 | |> with_var ) 31 | | Conv _ as param -> unspecialized param ~with_var 32 | | Comma_separated _ as param -> unspecialized param ~with_var 33 | ;; 34 | 35 | let make_key ~name = 36 | let length = String.length name in 37 | if length > 0 && name.[0] = '-' 38 | then name 39 | else if length = 1 40 | then "-" ^ name 41 | else "--" ^ name 42 | ;; 43 | 44 | module Arg_presence = struct 45 | type 'a t = 46 | | Required 47 | | Optional 48 | | Repeated 49 | | With_default of 50 | { param : 'a Ast.Param.t 51 | ; default : 'a 52 | } 53 | end 54 | 55 | let ( let* ) = Result.bind 56 | 57 | let make_docv param ~docv = 58 | let docv = Param_parser.docv param ~docv in 59 | Printf.sprintf "<%s>" docv 60 | ;; 61 | 62 | let make_doc (type a) ~doc ~arg_presence = 63 | Printf.sprintf 64 | "%s (%s)" 65 | doc 66 | (match (arg_presence : a Arg_presence.t) with 67 | | Required -> "required" 68 | | Optional -> "optional" 69 | | Repeated -> "repeated" 70 | | With_default { param; default } -> 71 | Printf.sprintf "default %s" (Param_parser.print param default)) 72 | ;; 73 | 74 | let compile 75 | : type a. 76 | a Arg_state.t 77 | -> ((Arg.key * Arg.spec * Arg.doc) list * Positional_state.t) Ast.or_error_msg 78 | = 79 | fun t -> 80 | let r = ref [] in 81 | let pos_state = ref [] in 82 | let pos_all_state = ref None in 83 | let emit_named s = r := s :: !r in 84 | let emit_pos pos = pos_state := Positional_state.One_pos.T pos :: !pos_state in 85 | let rec aux : type a. a Arg_state.t -> unit = 86 | fun t -> 87 | match t with 88 | | Return (_ : a) -> () 89 | | Map { x; f = _ } -> aux x 90 | | Both (a, b) -> 91 | aux a; 92 | aux b 93 | | Apply { f; x } -> 94 | aux f; 95 | aux x 96 | | Flag { names = hd :: tl; doc; var } -> 97 | let doc = make_doc ~doc ~arg_presence:Optional in 98 | hd :: tl 99 | |> List.iter (fun name -> emit_named (make_key ~name, Arg.Set var, " " ^ doc)) 100 | | Flag_count { names = hd :: tl; doc; var } -> 101 | let doc = make_doc ~doc ~arg_presence:Repeated in 102 | hd :: tl 103 | |> List.iter (fun name -> 104 | emit_named (make_key ~name, Arg.Unit (fun () -> incr var), " " ^ doc)) 105 | | Named { names = hd :: tl; param; docv; doc; var } -> 106 | let doc = make_doc ~doc ~arg_presence:Required in 107 | let docv = make_docv param ~docv in 108 | hd :: tl 109 | |> List.iter (fun name -> 110 | emit_named 111 | ( make_key ~name 112 | , make_arg_spec ~name param ~with_var:(fun s -> var := Some s) 113 | , docv ^ " " ^ doc )) 114 | | Named_multi { names = hd :: tl; param; docv; doc; rev_var } -> 115 | let doc = make_doc ~doc ~arg_presence:Repeated in 116 | let docv = make_docv param ~docv in 117 | hd :: tl 118 | |> List.iter (fun name -> 119 | emit_named 120 | ( make_key ~name 121 | , make_arg_spec ~name param ~with_var:(fun s -> rev_var := s :: !rev_var) 122 | , docv ^ " " ^ doc )) 123 | | Named_opt { names = hd :: tl; param; docv; doc; var } -> 124 | let doc = make_doc ~doc ~arg_presence:Optional in 125 | let docv = make_docv param ~docv in 126 | hd :: tl 127 | |> List.iter (fun name -> 128 | emit_named 129 | ( make_key ~name 130 | , make_arg_spec ~name param ~with_var:(fun s -> var := Some s) 131 | , docv ^ " " ^ doc )) 132 | | Named_with_default { names = hd :: tl; param; default; docv; doc; var } -> 133 | let doc = make_doc ~doc ~arg_presence:(With_default { param; default }) in 134 | let docv = make_docv param ~docv in 135 | hd :: tl 136 | |> List.iter (fun name -> 137 | emit_named 138 | ( make_key ~name 139 | , make_arg_spec ~name param ~with_var:(fun s -> var := Some s) 140 | , docv ^ " " ^ doc )) 141 | | Pos { pos; param; docv; doc; var } -> 142 | let doc = make_doc ~doc ~arg_presence:Required in 143 | emit_pos { pos; param; docv; doc; var } 144 | | Pos_opt { pos; param; docv; doc; var } -> 145 | let doc = make_doc ~doc ~arg_presence:Optional in 146 | emit_pos { pos; param; docv; doc; var } 147 | | Pos_with_default { pos; param; default; docv; doc; var } -> 148 | let doc = make_doc ~doc ~arg_presence:(With_default { param; default }) in 149 | emit_pos { pos; param; docv; doc; var } 150 | | Pos_all { param; docv; doc; rev_var } -> 151 | let doc = make_doc ~doc ~arg_presence:Repeated in 152 | pos_all_state := Some (Positional_state.Pos_all.T { param; docv; doc; rev_var }) 153 | in 154 | aux t; 155 | let spec_list = !r in 156 | let* positional_state = Positional_state.make ~pos:!pos_state ~pos_all:!pos_all_state in 157 | Ok (spec_list, positional_state) 158 | ;; 159 | 160 | type 'a t = 161 | { arg_state : 'a Arg_state.t 162 | ; spec : (Arg.key * Arg.spec * Arg.doc) list 163 | ; positional_state : Positional_state.t 164 | } 165 | 166 | let create arg = 167 | let arg_state = Arg_state.create arg in 168 | let* spec, positional_state = compile arg_state in 169 | Ok { arg_state; spec; positional_state } 170 | ;; 171 | 172 | let spec t = t.spec 173 | let positional_state t = t.positional_state 174 | let finalize t = Arg_state.finalize t.arg_state 175 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/parser_state.mli: -------------------------------------------------------------------------------- 1 | (** A mutable state that will collect parsing information. 2 | 3 | The strategy implemented by the cmdlang runner is to create such parser 4 | state, enrich it during a parsing phases using [stdlib.arg], and once this 5 | is done, return an expression suitable for evaluation. *) 6 | 7 | type 'a t 8 | 9 | (** {1 Initialization} 10 | 11 | In this part we allocate a parser state for a given parser. Once this is 12 | done, the parser must be enriched with information coming from the command 13 | line. *) 14 | 15 | val create : 'a Ast.Arg.t -> 'a t Ast.or_error_msg 16 | 17 | (** {1 Parsing} 18 | 19 | This part is what allows [stdlib.arg] to performs the expected side-effects 20 | within the state. *) 21 | 22 | val spec : _ t -> (Arg.key * Arg.spec * Arg.doc) list 23 | val positional_state : _ t -> Positional_state.t 24 | 25 | (** {1 Finalization} 26 | 27 | Once the parsing has been done, we can finalize the state and return an 28 | evaluation suitable for execution. *) 29 | 30 | val finalize : 'a t -> ('a Arg_runner.t, Arg_state.Parse_error.t) Result.t 31 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/positional_state.ml: -------------------------------------------------------------------------------- 1 | open! Import 2 | 3 | module One_pos = struct 4 | type 'a t = 5 | { pos : int 6 | ; param : 'a Ast.Param.t 7 | ; docv : string option 8 | ; doc : string 9 | ; var : 'a option ref 10 | } 11 | 12 | type packed = T : 'a t -> packed [@@unboxed] 13 | end 14 | 15 | module Pos_all = struct 16 | type 'a t = 17 | { param : 'a Ast.Param.t 18 | ; docv : string option 19 | ; doc : string 20 | ; rev_var : 'a list ref 21 | } 22 | 23 | type packed = T : 'a t -> packed [@@unboxed] 24 | end 25 | 26 | type t = 27 | { pos : One_pos.packed array 28 | ; pos_all : Pos_all.packed option 29 | ; mutable current_pos : int 30 | } 31 | 32 | let make_pos : One_pos.packed list -> One_pos.packed array Ast.or_error_msg = 33 | fun l -> 34 | let a = Array.of_list l in 35 | Array.sort (fun (One_pos.T { pos = a; _ }) (T { pos = b; _ }) -> compare a b) a; 36 | let skipped = 37 | Array.find_mapi (fun i (One_pos.T { pos; _ }) -> if i <> pos then Some i else None) a 38 | in 39 | match skipped with 40 | | None -> Ok a 41 | | Some i -> 42 | let message = 43 | Printf.sprintf 44 | "Attempted to declare a parser with a gap in its positional arguments.\n\ 45 | Positional argument %d is missing.\n" 46 | i 47 | in 48 | Error (`Msg message) 49 | ;; 50 | 51 | let ( let* ) = Result.bind 52 | 53 | let make ~pos ~pos_all = 54 | let* pos = make_pos pos in 55 | Ok { pos; pos_all; current_pos = 0 } 56 | ;; 57 | 58 | let anon_fun t anon = 59 | let current_pos = t.current_pos in 60 | t.current_pos <- succ current_pos; 61 | if current_pos < Array.length t.pos 62 | then ( 63 | let (One_pos.T { pos; param; docv = _; doc = _; var }) = t.pos.(current_pos) in 64 | assert (pos = current_pos); 65 | match Param_parser.eval param anon with 66 | | Ok a -> var := Some a 67 | | Error (`Msg error) -> 68 | raise 69 | (Arg.Bad 70 | (Printf.sprintf "Failed to parse the argument at position %d: %s" pos error))) 71 | else ( 72 | match t.pos_all with 73 | | None -> raise (Arg.Bad (Printf.sprintf "Unexpected positional argument %S" anon)) 74 | | Some (Pos_all.T { param; docv = _; doc = _; rev_var }) -> 75 | (match Param_parser.eval param anon with 76 | | Ok a -> rev_var := a :: !rev_var 77 | | Error (`Msg error) -> 78 | raise 79 | (Arg.Bad 80 | (Printf.sprintf "Positional argument %d %S: %s" current_pos anon error)))) 81 | ;; 82 | 83 | let usage_msg { pos; pos_all; current_pos = _ } = 84 | let pos = 85 | Array.to_list pos 86 | |> List.map (fun (One_pos.T { pos = _; param; docv; doc; var = _ }) -> 87 | let docv = Param_parser.docv param ~docv in 88 | Printf.sprintf " <%s> %s" docv doc) 89 | in 90 | let pos_all = 91 | match pos_all with 92 | | None -> [] 93 | | Some (Pos_all.T { param; docv; doc; rev_var = _ }) -> 94 | let docv = Param_parser.docv param ~docv in 95 | [ Printf.sprintf " <%s>* %s (listed)" docv doc ] 96 | in 97 | match pos @ pos_all with 98 | | [] -> None 99 | | _ -> Some ("Arguments:\n" ^ String.concat "\n" pos) 100 | ;; 101 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/src/positional_state.mli: -------------------------------------------------------------------------------- 1 | (** A mutable state that will collect parsing information for positional 2 | arguments. 3 | 4 | This state is compiled from the AST representation of the command line and 5 | is used to collect and store the values of positional arguments during the 6 | calls to [Arg.anon_fun]. *) 7 | 8 | module One_pos : sig 9 | type 'a t = 10 | { pos : int 11 | ; param : 'a Ast.Param.t 12 | ; docv : string option 13 | ; doc : string 14 | ; var : 'a option ref 15 | } 16 | 17 | type packed = T : 'a t -> packed [@@unboxed] 18 | end 19 | 20 | module Pos_all : sig 21 | type 'a t = 22 | { param : 'a Ast.Param.t 23 | ; docv : string option 24 | ; doc : string 25 | ; rev_var : 'a list ref 26 | } 27 | 28 | type packed = T : 'a t -> packed [@@unboxed] 29 | end 30 | 31 | type t = 32 | { pos : One_pos.packed array 33 | ; pos_all : Pos_all.packed option 34 | ; mutable current_pos : int 35 | } 36 | 37 | val make : pos:One_pos.packed list -> pos_all:Pos_all.packed option -> t Ast.or_error_msg 38 | 39 | (** Update the positional state based on the parsing of the next positional 40 | argument in the command line.*) 41 | val anon_fun : t -> Arg.anon_fun 42 | 43 | (** {1 Usage and help documentation} 44 | 45 | This section is dedicated to create contents to display for [--help] 46 | messages, such as in: 47 | 48 | {[ 49 | Usage: my_command [OPTIONS] [ARGUMENTS] 50 | 51 | ARGUMENTS: 52 | description of arg1 53 | description of arg2 54 | ]} *) 55 | 56 | (** Return [None] if no positional arguments are expected. *) 57 | val usage_msg : t -> string option 58 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_stdlib_runner_test) 3 | (public_name cmdlang-tests.cmdlang_stdlib_runner_test) 4 | (inline_tests) 5 | (flags 6 | :standard 7 | -w 8 | +a-4-40-41-42-44-45-48-66 9 | -warn-error 10 | +a 11 | -open 12 | Base 13 | -open 14 | Expect_test_helpers_base) 15 | (libraries 16 | base 17 | cmdlang 18 | cmdlang.ast 19 | cmdlang_stdlib_runner 20 | expect_test_helpers_core.expect_test_helpers_base) 21 | (instrumentation 22 | (backend bisect_ppx)) 23 | (lint 24 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 25 | (preprocess 26 | (pps 27 | -unused-code-warnings=force 28 | ppx_compare 29 | ppx_enumerate 30 | ppx_expect 31 | ppx_hash 32 | ppx_here 33 | ppx_let 34 | ppx_sexp_conv 35 | ppx_sexp_value))) 36 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/test/test__param_parser.ml: -------------------------------------------------------------------------------- 1 | module Ast = Cmdlang_ast.Ast 2 | module Param_parser = Cmdlang_stdlib_runner.Param_parser 3 | 4 | module Enum = struct 5 | type t = 6 | | A 7 | | B 8 | 9 | let to_string = function 10 | | A -> "a" 11 | | B -> "b" 12 | ;; 13 | end 14 | 15 | let%expect_test "print" = 16 | let test param a = print_endline (Param_parser.print param a) in 17 | test Ast.Param.String "Hello"; 18 | [%expect {| Hello |}]; 19 | test Ast.Param.Float 3.14; 20 | [%expect {| 3.14 |}]; 21 | test Ast.Param.Bool true; 22 | [%expect {| true |}]; 23 | test Ast.Param.File "path/to/file"; 24 | [%expect {| path/to/file |}]; 25 | let enum choices = 26 | Ast.Param.Enum { docv = None; choices; to_string = Enum.to_string } 27 | in 28 | test (enum [ "A", A ]) A; 29 | [%expect {| A |}]; 30 | test (enum [ "B", B ]) A; 31 | [%expect {| a |}]; 32 | test (enum [ "A", A ]) B; 33 | [%expect {| b |}]; 34 | () 35 | ;; 36 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/test/test__param_parser.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang_stdlib_runner/test/test__param_parser.mli -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/test/test__positional_state.ml: -------------------------------------------------------------------------------- 1 | module Positional_state = Cmdlang_stdlib_runner.Positional_state 2 | 3 | let%expect_test "anon_fun" = 4 | let no_pos = 5 | match Positional_state.make ~pos:[] ~pos_all:None with 6 | | Ok t -> t 7 | | Error _ -> assert false 8 | in 9 | require_does_raise [%here] (fun () -> 10 | (ignore (Positional_state.anon_fun no_pos "Hey" : unit) [@coverage off])); 11 | [%expect {| (Arg.Bad "Unexpected positional argument \"Hey\"") |}]; 12 | () 13 | ;; 14 | 15 | let%expect_test "pos_all" = 16 | let pos = 17 | match 18 | Positional_state.make 19 | ~pos:[] 20 | ~pos_all: 21 | (Some 22 | (Positional_state.Pos_all.T 23 | { param = Cmdlang_ast.Ast.Param.Int 24 | ; docv = None 25 | ; doc = "" 26 | ; rev_var = ref [] 27 | })) 28 | with 29 | | Ok t -> t 30 | | Error _ -> assert false 31 | in 32 | require_does_raise [%here] (fun () -> 33 | (ignore (Positional_state.anon_fun pos "Hey" : unit) [@coverage off])); 34 | [%expect 35 | {| (Arg.Bad "Positional argument 0 \"Hey\": invalid value \"Hey\" (not an int)") |}]; 36 | () 37 | ;; 38 | 39 | let%expect_test "usage_msg" = 40 | let pos = 41 | match 42 | Positional_state.make 43 | ~pos: 44 | [ T 45 | { pos = 0 46 | ; param = Cmdlang_ast.Ast.Param.Int 47 | ; docv = Some "Hello-INT" 48 | ; doc = "doc for pos0" 49 | ; var = ref None 50 | } 51 | ; T 52 | { pos = 1 53 | ; param = Cmdlang_ast.Ast.Param.Bool 54 | ; docv = None 55 | ; doc = "doc for pos1" 56 | ; var = ref None 57 | } 58 | ] 59 | ~pos_all: 60 | (Some 61 | (Positional_state.Pos_all.T 62 | { param = Cmdlang_ast.Ast.Param.Int 63 | ; docv = Some "INT" 64 | ; doc = "a sequence of integers" 65 | ; rev_var = ref [] 66 | })) 67 | with 68 | | Ok t -> t 69 | | Error _ -> assert false 70 | in 71 | print_endline (Positional_state.usage_msg pos |> Option.value_exn); 72 | [%expect 73 | {| 74 | Arguments: 75 | doc for pos0 76 | doc for pos1 77 | |}]; 78 | () 79 | ;; 80 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/test/test__positional_state.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang_stdlib_runner/test/test__positional_state.mli -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/test/test__stdlib_runner.ml: -------------------------------------------------------------------------------- 1 | module Command = Cmdlang.Command 2 | 3 | let%expect_test "eval_exit_code" = 4 | let arg = 5 | let open Command.Std in 6 | let+ arg = Arg.flag [ "flag" ] ~doc:"flag" in 7 | print_s [%sexp (arg : bool)] 8 | in 9 | let cmd = Command.make ~summary:"cmd" arg in 10 | let test argv = 11 | let code = 12 | Cmdlang_stdlib_runner.eval_exit_code 13 | cmd 14 | ~argv:(Array.of_list ("./main.exe" :: argv)) 15 | in 16 | print_endline (Printf.sprintf "[%d]" code) 17 | in 18 | test []; 19 | [%expect 20 | {| 21 | false 22 | [0] 23 | |}]; 24 | test [ "--help" ]; 25 | [%expect 26 | {| 27 | Usage: ./main.exe [OPTIONS] 28 | 29 | cmd 30 | 31 | Options: 32 | --flag flag (optional) 33 | -help Display this list of options 34 | --help Display this list of options 35 | [0] 36 | |}]; 37 | () 38 | ;; 39 | -------------------------------------------------------------------------------- /lib/cmdlang_stdlib_runner/test/test__stdlib_runner.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang_stdlib_runner/test/test__stdlib_runner.mli -------------------------------------------------------------------------------- /lib/cmdlang_to_base/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_to_base) 3 | (public_name cmdlang-to-base) 4 | (flags 5 | :standard 6 | -w 7 | +a-4-40-41-42-44-45-48-66 8 | -warn-error 9 | +a 10 | -open 11 | Base 12 | -open 13 | Cmdlang_ast) 14 | (libraries base cmdlang cmdlang_ast core.command) 15 | (instrumentation 16 | (backend bisect_ppx)) 17 | (lint 18 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 19 | (preprocess 20 | (pps 21 | -unused-code-warnings=force 22 | ppx_compare 23 | ppx_enumerate 24 | ppx_hash 25 | ppx_here 26 | ppx_let 27 | ppx_sexp_conv 28 | ppx_sexp_value))) 29 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/src/translate.mli: -------------------------------------------------------------------------------- 1 | (** Translate cmdlang parsers to core.command. *) 2 | 3 | (** {1 Configuration} 4 | 5 | The translation implemented in this module allows some configuration 6 | regarding the resulting command that you want to build. The goal of this 7 | configuration is to furnish assistance for complex multi-stages migrations from 8 | a backend to another. 9 | 10 | It is currently experimental, not well tested or documented, and expected to 11 | change in the future. *) 12 | 13 | module Config : sig 14 | type t 15 | 16 | val create 17 | : ?auto_add_one_dash_aliases:bool 18 | (** default to [false]. We recommend enabling one dash aliases to be 19 | used for migration path only. *) 20 | -> ?full_flags_required:bool 21 | (** default to [true]. Accepting flags unique prefixes makes the 22 | resulting behavior quite different from the other backends - we 23 | recommend [false] to be used for migration path only. *) 24 | -> unit 25 | -> t 26 | end 27 | 28 | (** {1 Param} *) 29 | 30 | val param : 'a Cmdlang.Command.Param.t -> config:Config.t -> 'a Command.Arg_type.t 31 | 32 | (** {1 Arg} *) 33 | 34 | val arg : 'a Cmdlang.Command.Arg.t -> config:Config.t -> 'a Command.Param.t 35 | 36 | (** {1 Command} *) 37 | 38 | val command_basic : ?config:Config.t -> (unit -> unit) Cmdlang.Command.t -> Command.t 39 | 40 | val command_or_error 41 | : ?config:Config.t 42 | -> (unit -> unit Or_error.t) Cmdlang.Command.t 43 | -> Command.t 44 | 45 | (** [unit] can be a convenient helper during a migration, however note that it 46 | is probably not quite right, due to the body of the command being evaluated 47 | as an argument. *) 48 | val command_unit : ?config:Config.t -> unit Cmdlang.Command.t -> Command.t 49 | 50 | module Utils : sig 51 | (** {1 Migration helpers} *) 52 | 53 | (** Print error and exit on error. Mimic [Core.Command.basic_or_error]. *) 54 | val or_error_handler : f:(unit -> unit Or_error.t) -> unit 55 | 56 | val command_unit_of_basic : (unit -> unit) Cmdlang.Command.t -> unit Cmdlang.Command.t 57 | 58 | val command_unit_of_or_error 59 | : (unit -> unit Or_error.t) Cmdlang.Command.t 60 | -> unit Cmdlang.Command.t 61 | end 62 | 63 | (** {1 Private} *) 64 | 65 | module Private : sig 66 | (** This module is exported for testing purposes only. Its signature may 67 | change in breaking ways without any notice. Do not use. *) 68 | 69 | module Arg : sig 70 | type 'a t = 'a Command.Param.t 71 | 72 | val translate : 'a Cmdlang_ast.Ast.Arg.t -> config:Config.t -> 'a t 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/bin/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names main_base main_climate) 3 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 4 | (libraries 5 | base_cram_test_command 6 | core_unix.command_unix 7 | cmdlang_to_climate 8 | climate) 9 | (instrumentation 10 | (backend bisect_ppx)) 11 | (preprocess no_preprocessing)) 12 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/bin/main_base.ml: -------------------------------------------------------------------------------- 1 | let () = Command_unix.run Base_cram_test_command.Cmd.main 2 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/bin/main_climate.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | Climate.Command.run 3 | (Cmdlang_to_climate.Translate.command Base_cram_test_command.Cmd.migrated) 4 | ;; 5 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (copy bin/main_base.exe main_base.exe)) 3 | 4 | (rule 5 | (copy bin/main_climate.exe main_climate.exe)) 6 | 7 | (cram 8 | (package cmdlang-tests) 9 | (deps 10 | (package cmdlang) 11 | main_base.exe 12 | main_climate.exe)) 13 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/run.t: -------------------------------------------------------------------------------- 1 | A simple migration plan from [core.command] to [climate]. 2 | 3 | Imagine we started from an original core command, defined as such: 4 | 5 | $ ./main_base.exe original -help 6 | A group of commands 7 | 8 | main_base.exe original SUBCOMMAND 9 | 10 | === subcommands === 11 | 12 | basic . A group of basic commands 13 | or-error . A group of or-error commands 14 | help . explain a given subcommand (perhaps recursively) 15 | 16 | 17 | $ ./main_base.exe original basic return 18 | 19 | $ ./main_base.exe original basic print -help 20 | A basic print command 21 | 22 | main_base.exe original basic print 23 | 24 | === flags === 25 | 26 | -arg ARG . my long arg 27 | [-help], -? . print this help text and exit 28 | 29 | 30 | $ ./main_base.exe original basic print -arg Hello 31 | Hello 32 | 33 | $ ./main_base.exe original or-error return 34 | 35 | $ ./main_base.exe original or-error print -help 36 | An or-error print command 37 | 38 | main_base.exe original or-error print 39 | 40 | === flags === 41 | 42 | [-arg ARG] . my long arg 43 | [-help], -? . print this help text and exit 44 | 45 | $ ./main_base.exe original or-error print -arg Hello 46 | Hello 47 | 48 | $ ./main_base.exe original or-error print 49 | This command fails during execution when the argument is missing. 50 | [1] 51 | 52 | The point of the migration plan is to avoid a single roll where all the commands 53 | are migrated at once, creating braking changes. For example: 54 | 55 | $ ./main_climate.exe basic print -arg Hello 56 | Unknown argument name: -a 57 | [124] 58 | 59 | OK, so as a first step, we will have to go over all CLI invocations and make 60 | sure that all the command names and arguments are fully provided. For example, 61 | the following partial invocation is supported by core.command: 62 | 63 | $ ./main_base.exe original bas pr -ar Hello 64 | Hello 65 | 66 | But will have no equivalent with climate, so we start by fixing them all. 67 | 68 | $ ./main_base.exe original basic print -arg Hello 69 | Hello 70 | 71 | Next, we can start migrating commands one by one, at which ever pace we prefer. 72 | We use a configuration for the translation that allows a transition phase during 73 | which arguments with single dashes are still supported. 74 | 75 | $ ./main_base.exe migration-step1 basic print -arg Hello 76 | Hello 77 | 78 | Whenever that code is rolled, we can patch invocations to use double dashes. 79 | 80 | $ ./main_base.exe migration-step1 basic print --arg Hello 81 | Hello 82 | 83 | If this is too confusing, one may wait for the full migration to change all 84 | flags at once. For example, the following command will fail: 85 | 86 | $ ./main_base.exe migration-step1 or-error print --arg Hello 87 | Error parsing command line: 88 | 89 | unknown flag --arg 90 | 91 | For usage information, run 92 | 93 | main_base.exe migration-step1 or-error print -help 94 | 95 | [1] 96 | 97 | OK let's say we've finished migrating all the commands: 98 | 99 | $ ./main_base.exe migration-step2 basic print --arg Hello 100 | Hello 101 | 102 | $ ./main_base.exe migration-step2 or-error print --arg Hello 103 | Hello 104 | 105 | $ ./main_base.exe migration-step2 or-error print 106 | This command fails during execution when the argument is missing 107 | [1] 108 | 109 | At this point, we are still using the core.command library, but we are ready to 110 | switch to climate, with no breaking changes. 111 | 112 | $ ./main_climate.exe basic return 113 | 114 | $ ./main_climate.exe basic print --arg Hello 115 | Hello 116 | 117 | $ ./main_climate.exe or-error return 118 | 119 | $ ./main_climate.exe or-error print --arg Hello 120 | Hello 121 | 122 | $ ./main_climate.exe or-error print 123 | This command fails during execution when the argument is missing 124 | [1] 125 | 126 | If you'd like, it is possible to add an additional step where we keep running 127 | core.command, but disallow single dashes for flags. 128 | 129 | $ ./main_base.exe migration-step3 basic print -arg Hello 130 | Error parsing command line: 131 | 132 | unknown flag -arg 133 | 134 | For usage information, run 135 | 136 | main_base.exe migration-step3 basic print -help 137 | 138 | [1] 139 | 140 | $ ./main_base.exe migration-step3 or-error print -arg Hello 141 | Error parsing command line: 142 | 143 | unknown flag -arg 144 | 145 | For usage information, run 146 | 147 | main_base.exe migration-step3 or-error print -help 148 | 149 | [1] 150 | 151 | Unfortunately, there is no way to disable the partial specification of commands 152 | with core.command so this part has to be carefully achieved. 153 | 154 | $ ./main_base.exe migration-step3 bas pr --arg Hello 155 | Hello 156 | 157 | However, the partial specification of flags can be disabled to prepare for the 158 | more strict invocations required by climate: 159 | 160 | $ ./main_base.exe migration-step3 basic print --ar Hello 161 | Error parsing command line: 162 | 163 | unknown flag --ar 164 | 165 | For usage information, run 166 | 167 | main_base.exe migration-step3 basic print -help 168 | 169 | [1] 170 | 171 | This additional step should permit isolating issues related to stale invocations 172 | from issues arising from the migration to climate. 173 | 174 | $ ./main_base.exe migration-step3 basic print --arg Hello 175 | Hello 176 | 177 | $ ./main_base.exe migration-step3 or-error print --arg Hello 178 | Hello 179 | 180 | $ ./main_base.exe migration-step3 or-error print 181 | This command fails during execution when the argument is missing 182 | [1] 183 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/src/cmd.ml: -------------------------------------------------------------------------------- 1 | let original_basic_print = 2 | Command.basic 3 | ~summary:"A basic print command" 4 | (let%map_open.Command arg = flag "arg" (required string) ~doc:"ARG my long arg" in 5 | fun () -> print_endline arg) 6 | ;; 7 | 8 | let original_basic_return = 9 | Command.basic 10 | ~summary:"A basic return command" 11 | (let%map_open.Command () = return () in 12 | fun () -> ()) 13 | ;; 14 | 15 | let original_basic = 16 | Command.group 17 | ~summary:"A group of basic commands" 18 | [ "print", original_basic_print; "return", original_basic_return ] 19 | ;; 20 | 21 | let original_or_error_print = 22 | Command.basic_or_error 23 | ~summary:"An or-error print command" 24 | (let%map_open.Command arg = flag "arg" (optional string) ~doc:"ARG my long arg" in 25 | fun () -> 26 | match arg with 27 | | None -> 28 | Or_error.error_string 29 | "This command fails during execution when the argument is missing." 30 | | Some arg -> 31 | print_endline arg; 32 | Or_error.return ()) 33 | ;; 34 | 35 | let original_or_error_return = 36 | Command.basic_or_error 37 | ~summary:"An or-error return command" 38 | (let%map_open.Command () = return () in 39 | fun () -> Or_error.return ()) 40 | ;; 41 | 42 | let original_or_error = 43 | Command.group 44 | ~summary:"A group of or-error commands" 45 | [ "print", original_or_error_print; "return", original_or_error_return ] 46 | ;; 47 | 48 | let original = 49 | Command.group 50 | ~summary:"A group of commands" 51 | [ "basic", original_basic; "or-error", original_or_error ] 52 | ;; 53 | 54 | let migrated_basic_print = 55 | let module Command = Cmdlang.Command in 56 | Command.make 57 | ~summary:"A basic command" 58 | (let%map_open.Command arg = 59 | Arg.named [ "arg" ] Param.string ~docv:"ARG" ~doc:"my long arg" 60 | in 61 | fun () -> print_endline arg) 62 | ;; 63 | 64 | let migrated_basic_return = 65 | let module Command = Cmdlang.Command in 66 | Command.make 67 | ~summary:"A basic command" 68 | (let%map_open.Command () = Arg.return () in 69 | fun () -> ()) 70 | ;; 71 | 72 | let migrated_basic = 73 | let module Command = Cmdlang.Command in 74 | Command.group 75 | ~summary:"A group of basic commands" 76 | [ "print", migrated_basic_print; "return", migrated_basic_return ] 77 | ;; 78 | 79 | let migrated_or_error_print = 80 | let module Command = Cmdlang.Command in 81 | Command.make 82 | ~summary:"An or-error print command" 83 | (let%map_open.Command arg = 84 | Arg.named_opt [ "arg" ] Param.string ~docv:"ARG" ~doc:"my long arg" 85 | in 86 | fun () -> 87 | match arg with 88 | | None -> 89 | Or_error.error_string 90 | "This command fails during execution when the argument is missing" 91 | | Some arg -> 92 | print_endline arg; 93 | Or_error.return ()) 94 | ;; 95 | 96 | let migrated_or_error_return = 97 | let module Command = Cmdlang.Command in 98 | Command.make 99 | ~summary:"An or-error return command" 100 | (let%map_open.Command () = Arg.return () in 101 | fun () -> Or_error.return ()) 102 | ;; 103 | 104 | let migrated_or_error = 105 | let module Command = Cmdlang.Command in 106 | Command.group 107 | ~summary:"A group of or-error commands" 108 | [ "print", migrated_or_error_print; "return", migrated_or_error_return ] 109 | ;; 110 | 111 | let migration_step1 = 112 | let config = 113 | Cmdlang_to_base.Translate.Config.create ~auto_add_one_dash_aliases:true () 114 | in 115 | let basic = Cmdlang_to_base.Translate.command_basic migrated_basic ~config in 116 | Command.group 117 | ~summary:"A group of commands partially migrated" 118 | [ "basic", basic; "or-error", original_or_error ] 119 | ;; 120 | 121 | let migration_step2 = 122 | let config = 123 | Cmdlang_to_base.Translate.Config.create ~auto_add_one_dash_aliases:true () 124 | in 125 | let basic = Cmdlang_to_base.Translate.command_basic migrated_basic ~config in 126 | let or_error = Cmdlang_to_base.Translate.command_or_error migrated_or_error ~config in 127 | Command.group 128 | ~summary:"A group of commands fully migrated" 129 | [ "basic", basic; "or-error", or_error ] 130 | ;; 131 | 132 | let migration_step3 = 133 | (* At this point, the default config may be used for the migration. *) 134 | let basic = Cmdlang_to_base.Translate.command_basic migrated_basic in 135 | let or_error = Cmdlang_to_base.Translate.command_or_error migrated_or_error in 136 | Command.group 137 | ~summary:"A group of commands fully and strictly migrated" 138 | [ "basic", basic; "or-error", or_error ] 139 | ;; 140 | 141 | let main = 142 | Command.group 143 | ~summary:"Multiple steps of migration" 144 | [ "original", original 145 | ; "migration-step1", migration_step1 146 | ; "migration-step2", migration_step2 147 | ; "migration-step3", migration_step3 148 | ] 149 | ;; 150 | 151 | let migrated = 152 | Cmdlang.Command.group 153 | ~summary:"Migrated command" 154 | [ "basic", Cmdlang_to_base.Translate.Utils.command_unit_of_basic migrated_basic 155 | ; ( "or-error" 156 | , Cmdlang_to_base.Translate.Utils.command_unit_of_or_error migrated_or_error ) 157 | ] 158 | ;; 159 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/src/cmd.mli: -------------------------------------------------------------------------------- 1 | val main : Command.t 2 | val migrated : unit Cmdlang.Command.t 3 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/cram/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name base_cram_test_command) 3 | (public_name cmdlang-tests.base_cram_test_command) 4 | (flags 5 | :standard 6 | -w 7 | +a-4-40-41-42-44-45-48-66 8 | -warn-error 9 | +a 10 | -open 11 | Base 12 | -open 13 | Stdio) 14 | (libraries base cmdlang cmdlang_to_base core.command stdio) 15 | (instrumentation 16 | (backend bisect_ppx)) 17 | (lint 18 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 19 | (preprocess 20 | (pps 21 | -unused-code-warnings=force 22 | ppx_compare 23 | ppx_enumerate 24 | ppx_hash 25 | ppx_here 26 | ppx_let 27 | ppx_sexp_conv 28 | ppx_sexp_value))) 29 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_to_base_test) 3 | (public_name cmdlang-tests.cmdlang_to_base_test) 4 | (inline_tests) 5 | (flags 6 | :standard 7 | -w 8 | +a-4-40-41-42-44-45-48-66 9 | -warn-error 10 | +a 11 | -open 12 | Base 13 | -open 14 | Expect_test_helpers_base) 15 | (libraries 16 | base 17 | cmdlang 18 | cmdlang_to_base 19 | core.command 20 | expect_test_helpers_core.expect_test_helpers_base) 21 | (instrumentation 22 | (backend bisect_ppx)) 23 | (lint 24 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 25 | (preprocess 26 | (pps 27 | -unused-code-warnings=force 28 | ppx_compare 29 | ppx_enumerate 30 | ppx_expect 31 | ppx_hash 32 | ppx_here 33 | ppx_let 34 | ppx_sexp_conv 35 | ppx_sexp_value))) 36 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/test__param.ml: -------------------------------------------------------------------------------- 1 | let%expect_test "param" = 2 | let config = Cmdlang_to_base.Translate.Config.create () in 3 | let conv (type a) (param : a Cmdlang.Command.Param.t) (sexp_of_a : a -> Sexp.t) params = 4 | let conv = Cmdlang_to_base.Translate.param param ~config in 5 | List.iter params ~f:(fun str -> 6 | print_s [%sexp (str : string), (Command.Arg_type.parse conv str : a Or_error.t)]) 7 | in 8 | conv Cmdlang.Command.Param.int [%sexp_of: int] [ ""; "a"; "0"; "42"; "-17" ]; 9 | [%expect 10 | {| 11 | ("" (Error (Failure "Int.of_string: \"\""))) 12 | (a (Error (Failure "Int.of_string: \"a\""))) 13 | (0 (Ok 0)) 14 | (42 (Ok 42)) 15 | (-17 (Ok -17)) 16 | |}]; 17 | () 18 | ;; 19 | -------------------------------------------------------------------------------- /lib/cmdlang_to_base/test/test__param.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang_to_base/test/test__param.mli -------------------------------------------------------------------------------- /lib/cmdlang_to_climate/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_to_climate) 3 | (public_name cmdlang-to-climate) 4 | (flags 5 | :standard 6 | -w 7 | +a-4-40-41-42-44-45-48-66 8 | -warn-error 9 | +a 10 | -open 11 | Cmdlang_ast) 12 | (libraries climate cmdlang cmdlang_ast) 13 | (instrumentation 14 | (backend bisect_ppx)) 15 | (lint 16 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 17 | (preprocess no_preprocessing)) 18 | -------------------------------------------------------------------------------- /lib/cmdlang_to_climate/src/translate.ml: -------------------------------------------------------------------------------- 1 | module Param = struct 2 | let rec translate : type a. a Ast.Param.t -> a Climate.Arg_parser.conv = function 3 | | Conv { docv; parse; print } -> 4 | { Climate.Arg_parser.parse 5 | ; print 6 | ; default_value_name = docv |> Option.value ~default:"VAL" 7 | ; completion = None 8 | } 9 | | String -> Climate.Arg_parser.string 10 | | Int -> Climate.Arg_parser.int 11 | | Float -> Climate.Arg_parser.float 12 | | Bool -> Climate.Arg_parser.bool 13 | | File -> Climate.Arg_parser.file 14 | | Enum { docv; choices = hd :: tl; to_string } -> 15 | let choices = hd :: tl in 16 | let str x = 17 | match List.find_opt (fun (_, y) -> y == x) choices with 18 | | Some (a, _) -> a 19 | | None -> to_string x 20 | in 21 | let eq a b = a == b || String.equal (str a) (str b) in 22 | Climate.Arg_parser.enum ?default_value_name:docv choices ~eq 23 | | Comma_separated t -> 24 | let { Climate.Arg_parser.parse; print; default_value_name; completion = _ } = 25 | translate t 26 | in 27 | let parse str = 28 | let ok, msgs = 29 | str 30 | |> String.split_on_char ',' 31 | |> List.partition_map (fun arg -> 32 | match parse arg with 33 | | Ok x -> Left x 34 | | Error (`Msg e) -> Right e) 35 | in 36 | match msgs with 37 | | [] -> Ok ok 38 | | _ :: _ -> Error (`Msg (String.concat ", " msgs)) 39 | in 40 | let print fmt = function 41 | | [] -> () 42 | | [ e ] -> print fmt e 43 | | hd :: (_ :: _ as tl) -> 44 | Format.fprintf fmt "%a," print hd; 45 | tl |> List.iter (fun i -> Format.fprintf fmt ",%a" print i) 46 | in 47 | { Climate.Arg_parser.parse; print; default_value_name; completion = None } 48 | ;; 49 | end 50 | 51 | module Arg = struct 52 | let fmt_doc ~doc = doc 53 | 54 | let make_doc : type a. doc:string -> param:a Ast.Param.t -> string = 55 | fun ~doc ~param -> 56 | match (param : _ Ast.Param.t) with 57 | | Conv _ | String | Int | Float | Bool | File 58 | | Enum { docv = _; choices = _; to_string = _ } 59 | | Comma_separated _ -> fmt_doc ~doc 60 | ;; 61 | 62 | let rec translate : type a. a Ast.Arg.t -> a Climate.Arg_parser.t = function 63 | | Return x -> Climate.Arg_parser.const x 64 | | Map { x; f } -> Climate.Arg_parser.map (translate x) ~f 65 | | Both (a, b) -> Climate.Arg_parser.both (translate a) (translate b) 66 | | Apply { f; x } -> Climate.Arg_parser.apply (translate f) (translate x) 67 | | Flag { names = hd :: tl; doc } -> 68 | let doc = fmt_doc ~doc in 69 | Climate.Arg_parser.flag ~doc (hd :: tl) 70 | | Flag_count { names = hd :: tl; doc } -> 71 | let doc = fmt_doc ~doc in 72 | Climate.Arg_parser.flag_count ~doc (hd :: tl) 73 | | Named { names = hd :: tl; param; docv; doc } -> 74 | let doc = make_doc ~doc ~param in 75 | Climate.Arg_parser.named_req 76 | ~doc 77 | ?value_name:docv 78 | (hd :: tl) 79 | (param |> Param.translate) 80 | | Named_multi { names = hd :: tl; param; docv; doc } -> 81 | let doc = make_doc ~doc ~param in 82 | Climate.Arg_parser.named_multi 83 | ~doc 84 | ?value_name:docv 85 | (hd :: tl) 86 | (param |> Param.translate) 87 | | Named_opt { names = hd :: tl; param; docv; doc } -> 88 | let doc = make_doc ~doc ~param in 89 | Climate.Arg_parser.named_opt 90 | ~doc 91 | ?value_name:docv 92 | (hd :: tl) 93 | (param |> Param.translate) 94 | | Named_with_default { names = hd :: tl; param; default; docv; doc } -> 95 | let doc = make_doc ~doc ~param in 96 | Climate.Arg_parser.named_with_default 97 | ~doc 98 | ?value_name:docv 99 | (hd :: tl) 100 | (param |> Param.translate) 101 | ~default 102 | | Pos { pos; param; docv; doc } -> 103 | let doc = make_doc ~doc ~param in 104 | Climate.Arg_parser.pos_req ~doc ?value_name:docv pos (param |> Param.translate) 105 | | Pos_opt { pos; param; docv; doc } -> 106 | let doc = make_doc ~doc ~param in 107 | Climate.Arg_parser.pos_opt ~doc ?value_name:docv pos (param |> Param.translate) 108 | | Pos_with_default { pos; param; default; docv; doc } -> 109 | let doc = make_doc ~doc ~param in 110 | Climate.Arg_parser.pos_with_default 111 | ~doc 112 | ?value_name:docv 113 | pos 114 | (param |> Param.translate) 115 | ~default 116 | | Pos_all { param; docv; doc } -> 117 | let doc = make_doc ~doc ~param in 118 | Climate.Arg_parser.pos_all ~doc ?value_name:docv (param |> Param.translate) 119 | ;; 120 | end 121 | 122 | module Command = struct 123 | let doc ~summary ~readme = 124 | match readme with 125 | | None -> summary 126 | | Some readme -> summary ^ "\n" ^ readme () 127 | ;; 128 | 129 | let rec to_command : type a. a Ast.Command.t -> a Climate.Command.t = 130 | fun command -> 131 | match command with 132 | | Make { arg; summary; readme } -> 133 | Climate.Command.singleton ~doc:(doc ~summary ~readme) (arg |> Arg.translate) 134 | | Group { default; summary; readme; subcommands } -> 135 | let cmds = subcommands |> List.map (fun (name, arg) -> to_subcommand arg ~name) in 136 | Climate.Command.group 137 | ?default_arg_parser:(default |> Option.map Arg.translate) 138 | ~doc:(doc ~summary ~readme) 139 | cmds 140 | 141 | and to_subcommand 142 | : type a. a Ast.Command.t -> name:string -> a Climate.Command.subcommand 143 | = 144 | fun command ~name -> Climate.Command.subcommand name (to_command command) 145 | ;; 146 | end 147 | 148 | module To_ast = Cmdlang.Command.Private.To_ast 149 | 150 | let param p = p |> To_ast.param |> Param.translate 151 | let arg a = a |> To_ast.arg |> Arg.translate 152 | let command a = a |> To_ast.command |> Command.to_command 153 | 154 | module Private = struct end 155 | -------------------------------------------------------------------------------- /lib/cmdlang_to_climate/src/translate.mli: -------------------------------------------------------------------------------- 1 | (** Translate cmdlang parsers to climate. 2 | 3 | The translation to climate is experimental, not well tested or documented, 4 | and doesn't support all features of climate. In particular there's currently 5 | no support to target the auto-completion features offered by climate. This 6 | is an area left for future work. More info 7 | {{:https://mbarbin.github.io/cmdlang/docs/explanation/future_plans/} here}. *) 8 | 9 | (** {1 Param} *) 10 | 11 | val param : 'a Cmdlang.Command.Param.t -> 'a Climate.Arg_parser.conv 12 | 13 | (** {1 Arg} *) 14 | 15 | val arg : 'a Cmdlang.Command.Arg.t -> 'a Climate.Arg_parser.t 16 | 17 | (** {1 Command} *) 18 | 19 | val command : 'a Cmdlang.Command.t -> 'a Climate.Command.t 20 | 21 | module Private : sig 22 | (** This module is exported for testing purposes only. Its signature may 23 | change in breaking ways without any notice. Do not use. *) 24 | end 25 | -------------------------------------------------------------------------------- /lib/cmdlang_to_climate/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_to_climate_test) 3 | (public_name cmdlang-tests.cmdlang_to_climate_test) 4 | (inline_tests) 5 | (flags 6 | :standard 7 | -w 8 | +a-4-40-41-42-44-45-48-66 9 | -warn-error 10 | +a 11 | -open 12 | Base 13 | -open 14 | Expect_test_helpers_base) 15 | (libraries 16 | base 17 | climate 18 | cmdlang 19 | cmdlang_to_climate 20 | expect_test_helpers_core.expect_test_helpers_base) 21 | (instrumentation 22 | (backend bisect_ppx)) 23 | (lint 24 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 25 | (preprocess 26 | (pps 27 | -unused-code-warnings=force 28 | ppx_compare 29 | ppx_enumerate 30 | ppx_expect 31 | ppx_hash 32 | ppx_here 33 | ppx_let 34 | ppx_sexp_conv 35 | ppx_sexp_value))) 36 | -------------------------------------------------------------------------------- /lib/cmdlang_to_climate/test/test__param.ml: -------------------------------------------------------------------------------- 1 | module Command = Cmdlang.Command 2 | 3 | let%expect_test "param" = 4 | let conv (type a) (param : a Command.Param.t) (sexp_of_a : a -> Sexp.t) params = 5 | let conv = Cmdlang_to_climate.Translate.param param in 6 | List.iter params ~f:(fun str -> 7 | print_s [%sexp (str : string), (conv.parse str : (a, [ `Msg of string ]) Result.t)]) 8 | in 9 | conv Command.Param.int [%sexp_of: int] [ ""; "a"; "0"; "42"; "-17" ]; 10 | [%expect 11 | {| 12 | ("" (Error (Msg "invalid value: \"\" (not an int)"))) 13 | (a (Error (Msg "invalid value: \"a\" (not an int)"))) 14 | (0 (Ok 0)) 15 | (42 (Ok 42)) 16 | (-17 (Ok -17)) 17 | |}]; 18 | () 19 | ;; 20 | 21 | module Color = struct 22 | type t = 23 | | Red 24 | | Green 25 | | Blue 26 | 27 | let all = [ Red; Green; Blue ] 28 | 29 | let to_string = function 30 | | Red -> "red" 31 | | Green -> "green" 32 | | Blue -> "blue" 33 | ;; 34 | end 35 | 36 | let%expect_test "enumerated" = 37 | let conv = 38 | Cmdlang_to_climate.Translate.param (Command.Param.enumerated (module Color)) 39 | in 40 | List.iter Color.all ~f:(fun color -> Stdlib.Format.printf "%a\n" conv.print color); 41 | [%expect 42 | {| 43 | red 44 | green 45 | blue 46 | |}]; 47 | (* Here we characterize what happens when [all] doesn't include all 48 | inhabitants of the enumerated [t]. *) 49 | let module Missing_color = struct 50 | include Color 51 | 52 | let all = [ Red; Green ] 53 | end 54 | in 55 | let conv = 56 | Cmdlang_to_climate.Translate.param (Command.Param.enumerated (module Missing_color)) 57 | in 58 | List.iter Missing_color.all ~f:(fun color -> 59 | Stdlib.Format.printf "%a\n" conv.print color); 60 | [%expect 61 | {| 62 | red 63 | green 64 | |}]; 65 | (match Stdlib.Format.printf "%a\n" conv.print Blue with 66 | | () -> assert false 67 | | exception Failure e -> print_endline e); 68 | [%expect 69 | {| Error in argument spec: Attempted to format an enum value as a string but the value does not appear in the enum declaration. Valid names for this enum are: red green |}]; 70 | () 71 | ;; 72 | -------------------------------------------------------------------------------- /lib/cmdlang_to_climate/test/test__param.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang_to_climate/test/test__param.mli -------------------------------------------------------------------------------- /lib/cmdlang_to_cmdliner/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_to_cmdliner) 3 | (public_name cmdlang-to-cmdliner) 4 | (flags 5 | :standard 6 | -w 7 | +a-4-40-41-42-44-45-48-66 8 | -warn-error 9 | +a 10 | -open 11 | Cmdlang_ast) 12 | (libraries cmdlang cmdlang_ast cmdliner) 13 | (instrumentation 14 | (backend bisect_ppx)) 15 | (lint 16 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 17 | (preprocess no_preprocessing)) 18 | -------------------------------------------------------------------------------- /lib/cmdlang_to_cmdliner/src/translate.mli: -------------------------------------------------------------------------------- 1 | (** Translate cmdlang parsers to cmdliner. *) 2 | 3 | (** {1 Param} *) 4 | 5 | val param : 'a Cmdlang.Command.Param.t -> 'a Cmdliner.Arg.conv 6 | 7 | (** {1 Arg} *) 8 | 9 | val arg : 'a Cmdlang.Command.Arg.t -> 'a Cmdliner.Term.t 10 | 11 | (** {1 Command} *) 12 | 13 | val command : ?version:string -> 'a Cmdlang.Command.t -> name:string -> 'a Cmdliner.Cmd.t 14 | 15 | (** {1 Private} *) 16 | 17 | module Private : sig 18 | (** This module is exported for testing purposes only. Its signature may 19 | change in breaking ways without any notice. Do not use. *) 20 | 21 | module Arg : sig 22 | val doc_of_param : doc:string -> param:'a Ast.Param.t -> string 23 | end 24 | 25 | module Command : sig 26 | val manpage_of_readme : readme:(unit -> string) -> [ `P of string ] list 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/cmdlang_to_cmdliner/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_to_cmdliner_test) 3 | (public_name cmdlang-tests.cmdlang_to_cmdliner_test) 4 | (inline_tests) 5 | (flags 6 | :standard 7 | -w 8 | +a-4-40-41-42-44-45-48-66 9 | -warn-error 10 | +a 11 | -open 12 | Base 13 | -open 14 | Expect_test_helpers_base) 15 | (libraries 16 | base 17 | cmdlang 18 | cmdlang_to_cmdliner 19 | cmdliner 20 | expect_test_helpers_core.expect_test_helpers_base) 21 | (instrumentation 22 | (backend bisect_ppx)) 23 | (lint 24 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 25 | (preprocess 26 | (pps 27 | -unused-code-warnings=force 28 | ppx_compare 29 | ppx_enumerate 30 | ppx_expect 31 | ppx_hash 32 | ppx_here 33 | ppx_let 34 | ppx_sexp_conv 35 | ppx_sexp_value))) 36 | -------------------------------------------------------------------------------- /lib/cmdlang_to_cmdliner/test/test__manpage_of_readme.ml: -------------------------------------------------------------------------------- 1 | module Manpage_block = struct 2 | type t = [ `P of string ] [@@deriving sexp_of] 3 | end 4 | 5 | let manpage_of_readme : readme:(unit -> string) -> Manpage_block.t list = 6 | Cmdlang_to_cmdliner.Translate.Private.Command.manpage_of_readme 7 | ;; 8 | 9 | let test str = 10 | let manpage = manpage_of_readme ~readme:(fun () -> str) in 11 | print_s [%sexp (manpage : Manpage_block.t list)] 12 | ;; 13 | 14 | let%expect_test "empty" = 15 | test ""; 16 | [%expect {| ((P "\n")) |}] 17 | ;; 18 | 19 | let%expect_test "white space" = 20 | test " "; 21 | [%expect {| ((P " ")) |}]; 22 | test " \n "; 23 | [%expect {| ((P " ")) |}]; 24 | test " \n \n "; 25 | [%expect {| ((P " ")) |}] 26 | ;; 27 | -------------------------------------------------------------------------------- /lib/cmdlang_to_cmdliner/test/test__manpage_of_readme.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang_to_cmdliner/test/test__manpage_of_readme.mli -------------------------------------------------------------------------------- /lib/cmdlang_to_cmdliner/test/test__param.ml: -------------------------------------------------------------------------------- 1 | module Command = Cmdlang.Command 2 | 3 | let%expect_test "param" = 4 | let conv (type a) (param : a Command.Param.t) (sexp_of_a : a -> Sexp.t) params = 5 | let conv = Cmdlang_to_cmdliner.Translate.param param in 6 | List.iter params ~f:(fun str -> 7 | print_s 8 | [%sexp 9 | (str : string) 10 | , (Cmdliner.Arg.conv_parser conv str : (a, [ `Msg of string ]) Result.t)]) 11 | in 12 | conv Command.Param.int [%sexp_of: int] [ ""; "a"; "0"; "42"; "-17" ]; 13 | [%expect 14 | {| 15 | ("" (Error (Msg "invalid value '', expected an integer"))) 16 | (a (Error (Msg "invalid value 'a', expected an integer"))) 17 | (0 (Ok 0)) 18 | (42 (Ok 42)) 19 | (-17 (Ok -17)) 20 | |}]; 21 | () 22 | ;; 23 | -------------------------------------------------------------------------------- /lib/cmdlang_to_cmdliner/test/test__param.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/lib/cmdlang_to_cmdliner/test/test__param.mli -------------------------------------------------------------------------------- /test/cram/README.md: -------------------------------------------------------------------------------- 1 | # Cmdlang cram tests 2 | 3 | In addition to expect tests we also have cram tests. The reason we have both is that some of the code is not easy to cover from pure OCaml code. 4 | 5 | It is not necessary that each piece of code in cmdlang be covered by both expect-tests AND cram-tests. In priority, we favor expect-tests (we prefer writing OCaml over bash). 6 | -------------------------------------------------------------------------------- /test/cram/bin/base/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main_base) 3 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 4 | (libraries cmdlang_to_base core_unix.command_unix cram_test_command) 5 | (instrumentation 6 | (backend bisect_ppx)) 7 | (preprocess no_preprocessing)) 8 | -------------------------------------------------------------------------------- /test/cram/bin/base/main_base.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | Command_unix.run (Cmdlang_to_base.Translate.command_unit Cram_test_command.Cmd.main) 3 | ;; 4 | -------------------------------------------------------------------------------- /test/cram/bin/climate/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main_climate) 3 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 4 | (libraries climate cmdlang_to_climate cram_test_command) 5 | (instrumentation 6 | (backend bisect_ppx)) 7 | (preprocess no_preprocessing)) 8 | -------------------------------------------------------------------------------- /test/cram/bin/climate/main_climate.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | Climate.Command.run (Cmdlang_to_climate.Translate.command Cram_test_command.Cmd.main) 3 | ;; 4 | -------------------------------------------------------------------------------- /test/cram/bin/cmdliner/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main_cmdliner) 3 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 4 | (libraries cmdlang_to_cmdliner cmdliner cram_test_command) 5 | (instrumentation 6 | (backend bisect_ppx)) 7 | (preprocess no_preprocessing)) 8 | -------------------------------------------------------------------------------- /test/cram/bin/cmdliner/main_cmdliner.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | let code = 3 | Cmdliner.Cmd.eval 4 | (Cmdlang_to_cmdliner.Translate.command 5 | Cram_test_command.Cmd.main 6 | ~name:Sys.argv.(0) 7 | ~version:"%%VERSION%%") 8 | in 9 | (* We disable coverage here because [bisect_ppx] instruments the out-edge of 10 | calls to [exit], which never returns. This creates false negatives in test 11 | coverage. We may revisit this decision in the future if the context 12 | changes. *) 13 | (exit code [@coverage off]) 14 | ;; 15 | -------------------------------------------------------------------------------- /test/cram/bin/stdlib-runner/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main_stdlib_runner) 3 | (flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a) 4 | (libraries cmdlang_stdlib_runner cram_test_command) 5 | (instrumentation 6 | (backend bisect_ppx)) 7 | (preprocess no_preprocessing)) 8 | -------------------------------------------------------------------------------- /test/cram/bin/stdlib-runner/main_stdlib_runner.ml: -------------------------------------------------------------------------------- 1 | let () = Cmdlang_stdlib_runner.run Cram_test_command.Cmd.main 2 | -------------------------------------------------------------------------------- /test/cram/const.t: -------------------------------------------------------------------------------- 1 | Checking the help when there are no arguments. 2 | 3 | $ ./main_base.exe return --help 4 | An empty command 5 | 6 | main_base.exe return 7 | 8 | === flags === 9 | 10 | [-help], -? . print this help text and exit 11 | 12 | 13 | $ ./main_climate.exe return --help 14 | An empty command 15 | 16 | Usage: ./main_climate.exe return [OPTION]… 17 | 18 | Options: 19 | -h, --help Show this help message. 20 | 21 | $ ./main_cmdliner.exe return --help=plain 22 | NAME 23 | ./main_cmdliner.exe-return - An empty command 24 | 25 | SYNOPSIS 26 | ./main_cmdliner.exe return [OPTION]… 27 | 28 | COMMON OPTIONS 29 | --help[=FMT] (default=auto) 30 | Show this help in format FMT. The value FMT must be one of auto, 31 | pager, groff or plain. With auto, the format is pager or plain 32 | whenever the TERM env var is dumb or undefined. 33 | 34 | --version 35 | Show version information. 36 | 37 | EXIT STATUS 38 | ./main_cmdliner.exe return exits with: 39 | 40 | 0 on success. 41 | 42 | 123 on indiscriminate errors reported on standard error. 43 | 44 | 124 on command line parsing errors. 45 | 46 | 125 on unexpected internal errors (bugs). 47 | 48 | SEE ALSO 49 | ./main_cmdliner.exe(1) 50 | 51 | $ ./main_stdlib_runner.exe return --help 52 | Usage: ./main_stdlib_runner.exe return [OPTIONS] 53 | 54 | An empty command 55 | 56 | Options: 57 | -help Display this list of options 58 | --help Display this list of options 59 | 60 | And run it too. 61 | 62 | $ ./main_base.exe return 63 | () 64 | 65 | $ ./main_climate.exe return 66 | () 67 | 68 | $ ./main_cmdliner.exe return 69 | () 70 | 71 | $ ./main_stdlib_runner.exe return 72 | () 73 | -------------------------------------------------------------------------------- /test/cram/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (copy bin/base/main_base.exe main_base.exe)) 3 | 4 | (rule 5 | (copy bin/climate/main_climate.exe main_climate.exe)) 6 | 7 | (rule 8 | (copy bin/cmdliner/main_cmdliner.exe main_cmdliner.exe)) 9 | 10 | (rule 11 | (copy bin/stdlib-runner/main_stdlib_runner.exe main_stdlib_runner.exe)) 12 | 13 | (cram 14 | (package cmdlang-tests) 15 | (deps 16 | (package cmdlang) 17 | main_base.exe 18 | main_climate.exe 19 | main_cmdliner.exe 20 | main_stdlib_runner.exe)) 21 | -------------------------------------------------------------------------------- /test/cram/flags.t: -------------------------------------------------------------------------------- 1 | Characterizing translation and behavior of various flag types. 2 | 3 | $ ./main_base.exe flags names --help 4 | various flags 5 | 6 | main_base.exe flags names 7 | 8 | === flags === 9 | 10 | [--long] . long 11 | [-a] . short 12 | [-help], -? . print this help text and exit 13 | 14 | 15 | $ ./main_climate.exe flags names --help 16 | various flags 17 | 18 | Usage: ./main_climate.exe flags names [OPTION]… 19 | 20 | Options: 21 | -a short 22 | --long long 23 | -h, --help Show this help message. 24 | 25 | $ ./main_cmdliner.exe flags names --help=plain 26 | NAME 27 | ./main_cmdliner.exe-flags-names - various flags 28 | 29 | SYNOPSIS 30 | ./main_cmdliner.exe flags names [-a] [--long] [OPTION]… 31 | 32 | OPTIONS 33 | -a short. 34 | 35 | --long 36 | long. 37 | 38 | COMMON OPTIONS 39 | --help[=FMT] (default=auto) 40 | Show this help in format FMT. The value FMT must be one of auto, 41 | pager, groff or plain. With auto, the format is pager or plain 42 | whenever the TERM env var is dumb or undefined. 43 | 44 | --version 45 | Show version information. 46 | 47 | EXIT STATUS 48 | ./main_cmdliner.exe flags names exits with: 49 | 50 | 0 on success. 51 | 52 | 123 on indiscriminate errors reported on standard error. 53 | 54 | 124 on command line parsing errors. 55 | 56 | 125 on unexpected internal errors (bugs). 57 | 58 | SEE ALSO 59 | ./main_cmdliner.exe(1) 60 | 61 | 62 | $ ./main_stdlib_runner.exe flags names --help 63 | Usage: ./main_stdlib_runner.exe flags names [OPTIONS] 64 | 65 | various flags 66 | 67 | Options: 68 | --long long (optional) 69 | -a short (optional) 70 | -help Display this list of options 71 | --help Display this list of options 72 | 73 | Cover the execution: 74 | 75 | $ ./main_stdlib_runner.exe flags names 76 | -------------------------------------------------------------------------------- /test/cram/main-help.t: -------------------------------------------------------------------------------- 1 | In this test we monitor the global help page that is generated at the root of 2 | the executable for each backend. 3 | 4 | $ ./main_base.exe --help 5 | Cram Test Command 6 | 7 | main_base.exe SUBCOMMAND 8 | 9 | === subcommands === 10 | 11 | basic . Basic types 12 | doc . Testing documentation features 13 | enum . Enum types 14 | flags . flags 15 | group . A group command with a default 16 | named . Named arguments 17 | return . An empty command 18 | version . print version information 19 | help . explain a given subcommand (perhaps recursively) 20 | 21 | 22 | $ ./main_climate.exe --help 23 | Cram Test Command 24 | 25 | Usage: ./main_climate.exe [COMMAND] 26 | ./main_climate.exe [OPTION]… 27 | 28 | Options: 29 | -h, --help Show this help message. 30 | 31 | Commands: 32 | basic Basic types 33 | doc Testing documentation features 34 | 35 | This group is dedicated to testing documentation features. 36 | 37 | enum Enum types 38 | flags flags 39 | group A group command with a default 40 | named Named arguments 41 | return An empty command 42 | 43 | $ ./main_cmdliner.exe --help=plain 44 | NAME 45 | ./main_cmdliner.exe - Cram Test Command 46 | 47 | SYNOPSIS 48 | ./main_cmdliner.exe COMMAND … 49 | 50 | COMMANDS 51 | basic COMMAND … 52 | Basic types 53 | 54 | doc COMMAND … 55 | Testing documentation features 56 | 57 | enum COMMAND … 58 | Enum types 59 | 60 | flags COMMAND … 61 | flags 62 | 63 | group [COMMAND] … 64 | A group command with a default 65 | 66 | named COMMAND … 67 | Named arguments 68 | 69 | return [OPTION]… 70 | An empty command 71 | 72 | COMMON OPTIONS 73 | --help[=FMT] (default=auto) 74 | Show this help in format FMT. The value FMT must be one of auto, 75 | pager, groff or plain. With auto, the format is pager or plain 76 | whenever the TERM env var is dumb or undefined. 77 | 78 | --version 79 | Show version information. 80 | 81 | EXIT STATUS 82 | ./main_cmdliner.exe exits with: 83 | 84 | 0 on success. 85 | 86 | 123 on indiscriminate errors reported on standard error. 87 | 88 | 124 on command line parsing errors. 89 | 90 | 125 on unexpected internal errors (bugs). 91 | 92 | 93 | $ ./main_stdlib_runner.exe --help 94 | Usage: ./main_stdlib_runner.exe [OPTIONS] 95 | 96 | Cram Test Command 97 | 98 | Subcommands: 99 | basic Basic types 100 | doc Testing documentation features 101 | enum Enum types 102 | flags flags 103 | group A group command with a default 104 | named Named arguments 105 | return An empty command 106 | 107 | Options: 108 | -help Display this list of options 109 | --help Display this list of options 110 | -------------------------------------------------------------------------------- /test/cram/named-opt.t: -------------------------------------------------------------------------------- 1 | In this test we monitor the behavior and doc related to the `named_opt` construct. 2 | 3 | Let's start with characterizing whether and how the default value appears in the help page. 4 | 5 | $ ./main_base.exe named opt string-with-docv --help 6 | Named_opt__string_with_docv 7 | 8 | main_base.exe named opt string-with-docv 9 | 10 | === flags === 11 | 12 | [--who WHO] . Hello WHO? 13 | [-help], -? . print this help text and exit 14 | 15 | 16 | $ ./main_climate.exe named opt string-with-docv --help 17 | Named_opt__string_with_docv 18 | 19 | Usage: ./main_climate.exe named opt string-with-docv [OPTION]… 20 | 21 | Options: 22 | --who Hello WHO? 23 | -h, --help Show this help message. 24 | 25 | $ ./main_cmdliner.exe named opt string-with-docv --help=plain 26 | NAME 27 | ./main_cmdliner.exe-named-opt-string-with-docv - 28 | Named_opt__string_with_docv 29 | 30 | SYNOPSIS 31 | ./main_cmdliner.exe named opt string-with-docv [--who=WHO] [OPTION]… 32 | 33 | OPTIONS 34 | --who=WHO 35 | Hello WHO? 36 | 37 | COMMON OPTIONS 38 | --help[=FMT] (default=auto) 39 | Show this help in format FMT. The value FMT must be one of auto, 40 | pager, groff or plain. With auto, the format is pager or plain 41 | whenever the TERM env var is dumb or undefined. 42 | 43 | --version 44 | Show version information. 45 | 46 | EXIT STATUS 47 | ./main_cmdliner.exe named opt string-with-docv exits with: 48 | 49 | 0 on success. 50 | 51 | 123 on indiscriminate errors reported on standard error. 52 | 53 | 124 on command line parsing errors. 54 | 55 | 125 on unexpected internal errors (bugs). 56 | 57 | SEE ALSO 58 | ./main_cmdliner.exe(1) 59 | 60 | 61 | $ ./main_stdlib_runner.exe named opt string-with-docv --help 62 | Usage: ./main_stdlib_runner.exe named opt string-with-docv [OPTIONS] 63 | 64 | Named_opt__string_with_docv 65 | 66 | Options: 67 | --who Hello WHO? (optional) 68 | -help Display this list of options 69 | --help Display this list of options 70 | 71 | -- 72 | 73 | $ ./main_base.exe named opt string-with-docv 74 | 75 | $ ./main_climate.exe named opt string-with-docv 76 | 77 | $ ./main_cmdliner.exe named opt string-with-docv 78 | 79 | $ ./main_stdlib_runner.exe named opt string-with-docv 80 | 81 | -- 82 | 83 | $ ./main_base.exe named opt string-with-docv --who Alice 84 | Hello Alice 85 | 86 | $ ./main_climate.exe named opt string-with-docv --who Alice 87 | Hello Alice 88 | 89 | $ ./main_cmdliner.exe named opt string-with-docv --who Alice 90 | Hello Alice 91 | 92 | $ ./main_stdlib_runner.exe named opt string-with-docv --who Alice 93 | Hello Alice 94 | 95 | Characterizing the flag documentation when the `docv` parameter is not supplied. 96 | 97 | $ ./main_base.exe named opt string-without-docv --help 98 | Named_opt__string_without_docv 99 | 100 | main_base.exe named opt string-without-docv 101 | 102 | === flags === 103 | 104 | [--who STRING] . Hello WHO? 105 | [-help], -? . print this help text and exit 106 | 107 | 108 | $ ./main_climate.exe named opt string-without-docv --help 109 | Named_opt__string_without_docv 110 | 111 | Usage: ./main_climate.exe named opt string-without-docv [OPTION]… 112 | 113 | Options: 114 | --who Hello WHO? 115 | -h, --help Show this help message. 116 | 117 | $ ./main_cmdliner.exe named opt string-without-docv --help=plain 118 | NAME 119 | ./main_cmdliner.exe-named-opt-string-without-docv - 120 | Named_opt__string_without_docv 121 | 122 | SYNOPSIS 123 | ./main_cmdliner.exe named opt string-without-docv [--who=STRING] 124 | [OPTION]… 125 | 126 | OPTIONS 127 | --who=STRING 128 | Hello WHO? 129 | 130 | COMMON OPTIONS 131 | --help[=FMT] (default=auto) 132 | Show this help in format FMT. The value FMT must be one of auto, 133 | pager, groff or plain. With auto, the format is pager or plain 134 | whenever the TERM env var is dumb or undefined. 135 | 136 | --version 137 | Show version information. 138 | 139 | EXIT STATUS 140 | ./main_cmdliner.exe named opt string-without-docv exits with: 141 | 142 | 0 on success. 143 | 144 | 123 on indiscriminate errors reported on standard error. 145 | 146 | 124 on command line parsing errors. 147 | 148 | 125 on unexpected internal errors (bugs). 149 | 150 | SEE ALSO 151 | ./main_cmdliner.exe(1) 152 | 153 | 154 | $ ./main_stdlib_runner.exe named opt string-without-docv --help 155 | Usage: ./main_stdlib_runner.exe named opt string-without-docv [OPTIONS] 156 | 157 | Named_opt__string_without_docv 158 | 159 | Options: 160 | --who Hello WHO? (optional) 161 | -help Display this list of options 162 | --help Display this list of options 163 | 164 | -------------------------------------------------------------------------------- /test/cram/src/cmd.mli: -------------------------------------------------------------------------------- 1 | val main : unit Command.t 2 | -------------------------------------------------------------------------------- /test/cram/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cram_test_command) 3 | (public_name cmdlang-tests.cram_test_command) 4 | (flags 5 | :standard 6 | -w 7 | +a-4-40-41-42-44-45-48-66 8 | -warn-error 9 | +a 10 | -open 11 | Base 12 | -open 13 | Stdio 14 | -open 15 | Cmdlang) 16 | (libraries base cmdlang stdio) 17 | (instrumentation 18 | (backend bisect_ppx)) 19 | (lint 20 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 21 | (preprocess 22 | (pps 23 | -unused-code-warnings=force 24 | ppx_compare 25 | ppx_enumerate 26 | ppx_hash 27 | ppx_here 28 | ppx_let 29 | ppx_sexp_conv 30 | ppx_sexp_value))) 31 | -------------------------------------------------------------------------------- /test/expect/README.md: -------------------------------------------------------------------------------- 1 | # Cmdlang expect tests 2 | 3 | ## Translation 4 | 5 | In this directory we aim at having comprehensive coverage for the translation of cmdlang specifications to the supported backends. 6 | 7 | We also aim at fully characterizing the behavior of the parsers that are obtained that way. We try to do a good job at capturing in the tests the subtle semantic differences that may be encountered depending on the backend chosen. 8 | 9 | ### Status? 10 | 11 | We're currently increasing coverage as we go. We plan on reaching 100% coverage at the end of this process. Stay tune for progress on this front! 12 | -------------------------------------------------------------------------------- /test/expect/arg_test.ml: -------------------------------------------------------------------------------- 1 | module Core_command = Command 2 | 3 | type 'a t = 4 | { arg : 'a Cmdlang.Command.Arg.t 5 | ; base : ('a Core_command.Param.t, Exn.t) Result.t 6 | ; climate : ('a Climate.Arg_parser.t, Exn.t) Result.t 7 | ; cmdliner : ('a Cmdliner.Term.t, Exn.t) Result.t 8 | } 9 | 10 | let create arg = 11 | let base = 12 | let config = Cmdlang_to_base.Translate.Config.create () in 13 | match Cmdlang_to_base.Translate.arg arg ~config with 14 | | param -> Ok param 15 | | exception e -> Error e [@coverage off] 16 | in 17 | let climate = 18 | match Cmdlang_to_climate.Translate.arg arg with 19 | | arg_parser -> Ok arg_parser 20 | | exception e -> Error e [@coverage off] 21 | in 22 | let cmdliner = 23 | match Cmdlang_to_cmdliner.Translate.arg arg with 24 | | term -> Ok term 25 | | exception e -> Error e [@coverage off] 26 | in 27 | { arg; base; climate; cmdliner } 28 | ;; 29 | 30 | module Backend = struct 31 | type t = 32 | | Climate 33 | | Cmdliner 34 | | Core_command 35 | | Stdlib_runner 36 | [@@deriving enumerate, sexp_of] 37 | 38 | let to_string t = Sexp.to_string (sexp_of_t t) 39 | end 40 | 41 | module Command_line = struct 42 | type t = 43 | { prog : string 44 | ; args : string list 45 | } 46 | end 47 | 48 | let eval_base t { Command_line.prog = _; args } = 49 | match t.base with 50 | | Error e -> print_s [%sexp "Translation Raised", (e : Exn.t)] [@coverage off] 51 | | Ok param -> 52 | (match Core_command.Param.parse param args with 53 | | Ok () -> () 54 | | Error e -> print_s [%sexp "Evaluation Failed", (e : Error.t)] 55 | | exception e -> print_s [%sexp "Evaluation Raised", (e : Exn.t)] [@coverage off]) 56 | ;; 57 | 58 | let eval_climate t { Command_line.prog; args } = 59 | match t.climate with 60 | | Error e -> print_s [%sexp "Translation Raised", (e : Exn.t)] [@coverage off] 61 | | Ok arg_parser -> 62 | (match 63 | let cmd = Climate.Command.singleton arg_parser in 64 | Climate.For_test.eval_result ~program_name:prog cmd args 65 | with 66 | | Ok () -> () 67 | | Error e -> 68 | print_string "Evaluation Failed: "; 69 | Climate_non_ret.print e 70 | | exception e -> print_s [%sexp "Evaluation Raised", (e : Exn.t)] [@coverage off]) 71 | ;; 72 | 73 | let eval_cmdliner t { Command_line.prog; args } = 74 | let args = 75 | List.map args ~f:(function 76 | | "--help" -> "--help=plain" 77 | | arg -> arg) 78 | in 79 | match t.cmdliner with 80 | | Error e -> print_s [%sexp "Translation Raised", (e : Exn.t)] [@coverage off] 81 | | Ok term -> 82 | (match 83 | let cmd = Cmdliner.Cmd.v (Cmdliner.Cmd.info prog) term in 84 | Cmdliner.Cmd.eval cmd ~argv:(Array.of_list (prog :: args)) 85 | with 86 | | 0 -> () 87 | | exit_code -> 88 | print_s [%sexp "Evaluation Failed", { exit_code : int }] [@coverage off] 89 | | exception e -> print_s [%sexp "Evaluation Raised", (e : Exn.t)] [@coverage off]) 90 | ;; 91 | 92 | let eval_stdlib_runner t { Command_line.prog; args } = 93 | let command = Cmdlang.Command.make t.arg ~summary:"eval-stdlib-runner" in 94 | match Cmdlang_stdlib_runner.eval command ~argv:(Array.of_list (prog :: args)) with 95 | | Ok () -> () 96 | | Error (`Help msg) -> print_endline msg 97 | | Error (`Bad msg) -> 98 | Stdlib.print_string msg; 99 | print_s [%sexp "Evaluation Failed", { exit_code = (2 : int) }] [@coverage off] 100 | | exception e -> print_s [%sexp "Evaluation Raised", (e : Exn.t)] [@coverage off] 101 | ;; 102 | 103 | let eval_all t command_line = 104 | List.iter Backend.all ~f:(fun backend -> 105 | print_endline 106 | (Printf.sprintf 107 | "----------------------------------------------------- %s" 108 | (Backend.to_string backend)); 109 | (match backend with 110 | | Climate -> eval_climate t command_line 111 | | Cmdliner -> eval_cmdliner t command_line 112 | | Core_command -> eval_base t command_line 113 | | Stdlib_runner -> eval_stdlib_runner t command_line); 114 | Stdlib.(flush stdout); 115 | Stdlib.(flush stderr)); 116 | () 117 | ;; 118 | -------------------------------------------------------------------------------- /test/expect/arg_test.mli: -------------------------------------------------------------------------------- 1 | (** This module allows to stage the translation of a cmdlang argument into 2 | different backends for testing. *) 3 | 4 | type 'a t 5 | 6 | val create : 'a Cmdlang.Command.Arg.t -> 'a t 7 | 8 | (** {1 Evaluation} *) 9 | 10 | module Command_line : sig 11 | type t = 12 | { prog : string 13 | ; args : string list 14 | } 15 | end 16 | 17 | (** Evaluate all backends and print a full trace on standard channels for use in 18 | expect tests. *) 19 | val eval_all : unit t -> Command_line.t -> unit 20 | -------------------------------------------------------------------------------- /test/expect/climate_non_ret.ml: -------------------------------------------------------------------------------- 1 | type t = Climate.For_test.Non_ret.t 2 | 3 | let print : t -> unit = function 4 | | Help spec -> Climate.For_test.print_help_spec spec 5 | | Manpage { spec; prose } -> Climate.For_test.print_manpage spec prose [@coverage off] 6 | | Reentrant_query { suggestions } -> 7 | print_s [%sexp Reentrant_query { suggestions : string list }] [@coverage off] 8 | | Parse_error parse_error -> 9 | print_endline (Climate.For_test.Parse_error.to_string parse_error) 10 | | Generate_completion_script { completion_script } -> 11 | print_s 12 | [%sexp Generate_completion_script { completion_script : string }] [@coverage off] 13 | ;; 14 | -------------------------------------------------------------------------------- /test/expect/climate_non_ret.mli: -------------------------------------------------------------------------------- 1 | (** This is a helper that allows printing climate errors in the expect tests. *) 2 | 3 | type t = Climate.For_test.Non_ret.t 4 | 5 | val print : t -> unit 6 | -------------------------------------------------------------------------------- /test/expect/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cmdlang_expect_tests) 3 | (public_name cmdlang-tests.expect-tests) 4 | (inline_tests) 5 | (flags 6 | :standard 7 | -w 8 | +a-4-40-41-42-44-45-48-66 9 | -warn-error 10 | +a 11 | -open 12 | Base 13 | -open 14 | Expect_test_helpers_base) 15 | (libraries 16 | base 17 | climate 18 | cmdlang 19 | cmdlang-stdlib-runner 20 | cmdlang-to-base 21 | cmdlang-to-climate 22 | cmdlang-to-cmdliner 23 | cmdliner 24 | core.command 25 | core_unix.command_unix 26 | expect_test_helpers_core.expect_test_helpers_base 27 | loc) 28 | (instrumentation 29 | (backend bisect_ppx)) 30 | (lint 31 | (pps ppx_js_style -allow-let-operators -check-doc-comments)) 32 | (preprocess 33 | (pps 34 | -unused-code-warnings=force 35 | ppx_compare 36 | ppx_enumerate 37 | ppx_expect 38 | ppx_hash 39 | ppx_here 40 | ppx_let 41 | ppx_sexp_conv 42 | ppx_sexp_value))) 43 | -------------------------------------------------------------------------------- /test/expect/test__applicative_operations.ml: -------------------------------------------------------------------------------- 1 | module Command = Cmdlang.Command 2 | 3 | let%expect_test "const" = 4 | let test = 5 | Arg_test.create 6 | (let%map_open.Command string = Arg.return "hello" in 7 | print_endline string) 8 | in 9 | Arg_test.eval_all test { prog = "test"; args = [] }; 10 | [%expect 11 | {| 12 | ----------------------------------------------------- Climate 13 | hello 14 | ----------------------------------------------------- Cmdliner 15 | hello 16 | ----------------------------------------------------- Core_command 17 | hello 18 | ----------------------------------------------------- Stdlib_runner 19 | hello 20 | |}]; 21 | () 22 | ;; 23 | 24 | let%expect_test "map" = 25 | let test = 26 | Arg_test.create 27 | (let%map_open.Command v = 28 | Arg.pos ~pos:0 Param.string ~doc:"an integer" |> Arg.map ~f:Int.of_string_opt 29 | in 30 | print_s [%sexp (v : int option)]) 31 | in 32 | Arg_test.eval_all test { prog = "test"; args = [ "0" ] }; 33 | [%expect 34 | {| 35 | ----------------------------------------------------- Climate 36 | (0) 37 | ----------------------------------------------------- Cmdliner 38 | (0) 39 | ----------------------------------------------------- Core_command 40 | (0) 41 | ----------------------------------------------------- Stdlib_runner 42 | (0) 43 | |}]; 44 | Arg_test.eval_all test { prog = "test"; args = [ "not-an-int" ] }; 45 | [%expect 46 | {| 47 | ----------------------------------------------------- Climate 48 | () 49 | ----------------------------------------------------- Cmdliner 50 | () 51 | ----------------------------------------------------- Core_command 52 | () 53 | ----------------------------------------------------- Stdlib_runner 54 | () 55 | |}]; 56 | Arg_test.eval_all test { prog = "test"; args = [ "42" ] }; 57 | [%expect 58 | {| 59 | ----------------------------------------------------- Climate 60 | (42) 61 | ----------------------------------------------------- Cmdliner 62 | (42) 63 | ----------------------------------------------------- Core_command 64 | (42) 65 | ----------------------------------------------------- Stdlib_runner 66 | (42) 67 | |}]; 68 | () 69 | ;; 70 | 71 | let%expect_test "apply" = 72 | let module Operator = struct 73 | type t = 74 | | Succ 75 | | Pred 76 | [@@deriving enumerate] 77 | 78 | let to_string = function 79 | | Succ -> "succ" 80 | | Pred -> "pred" 81 | ;; 82 | 83 | let apply t i = 84 | match t with 85 | | Succ -> i + 1 86 | | Pred -> i - 1 87 | ;; 88 | end 89 | in 90 | let test = 91 | let open Command.Std in 92 | Arg_test.create 93 | (let op = 94 | Arg.pos ~pos:0 (Param.enumerated (module Operator)) ~doc:"an operator" 95 | |> Arg.map ~f:Operator.apply 96 | and v = Arg.pos_with_default ~pos:1 Param.int ~default:0 ~doc:"an integer" in 97 | Arg.map (Arg.apply op v) ~f:(fun v -> print_s [%sexp (v : int)])) 98 | in 99 | Arg_test.eval_all test { prog = "test"; args = [ "succ"; "0" ] }; 100 | [%expect 101 | {| 102 | ----------------------------------------------------- Climate 103 | 1 104 | ----------------------------------------------------- Cmdliner 105 | 1 106 | ----------------------------------------------------- Core_command 107 | 1 108 | ----------------------------------------------------- Stdlib_runner 109 | 1 110 | |}]; 111 | Arg_test.eval_all test { prog = "test"; args = [ "pred"; "42" ] }; 112 | [%expect 113 | {| 114 | ----------------------------------------------------- Climate 115 | 41 116 | ----------------------------------------------------- Cmdliner 117 | 41 118 | ----------------------------------------------------- Core_command 119 | 41 120 | ----------------------------------------------------- Stdlib_runner 121 | 41 122 | |}]; 123 | () 124 | ;; 125 | -------------------------------------------------------------------------------- /test/expect/test__applicative_operations.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__applicative_operations.mli -------------------------------------------------------------------------------- /test/expect/test__cmd_name_with_underscore.ml: -------------------------------------------------------------------------------- 1 | module Command = Cmdlang.Command 2 | 3 | let return = 4 | Command.make 5 | ~summary:"return" 6 | (let open Command.Std in 7 | let+ () = Arg.return () in 8 | ()) 9 | ;; 10 | 11 | let group = Command.group ~summary:"group" [ "name_with_underscore", return ] 12 | 13 | let%expect_test "base" = 14 | (* core.command rejects subcommand names containing an underscore. *) 15 | require_does_raise [%here] (fun () -> 16 | Command_unix.run (Cmdlang_to_base.Translate.command_unit group)); 17 | [%expect 18 | {| 19 | (Failure 20 | "subcommand name_with_underscore contains an underscore. Use a dash instead.") 21 | |}]; 22 | () 23 | ;; 24 | 25 | let%expect_test "climate" = 26 | (* In climate, subcommand names containing an underscore are valid. *) 27 | Climate.Command.eval 28 | (Cmdlang_to_climate.Translate.command group) 29 | ~program_name:(Literal "./main.exe") 30 | [ "name_with_underscore" ]; 31 | [%expect {||}]; 32 | () 33 | ;; 34 | 35 | let%expect_test "cmdliner" = 36 | (* In cmdliner, subcommand names containing an underscore are valid. *) 37 | Cmdliner.Cmd.eval 38 | (Cmdlang_to_cmdliner.Translate.command group ~name:"./main.exe") 39 | ~argv:[| "./main.exe"; "name_with_underscore" |] 40 | |> Stdlib.print_int; 41 | [%expect {| 0 |}]; 42 | () 43 | ;; 44 | 45 | let%expect_test "stdlib-runner" = 46 | (* In the stdlib-runner, subcommand names containing an underscore are valid. *) 47 | (match 48 | Cmdlang_stdlib_runner.eval group ~argv:[| "./main.exe"; "name_with_underscore" |] 49 | with 50 | | Ok () -> () 51 | | Error _ -> assert false); 52 | [%expect {||}]; 53 | () 54 | ;; 55 | -------------------------------------------------------------------------------- /test/expect/test__cmd_name_with_underscore.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__cmd_name_with_underscore.mli -------------------------------------------------------------------------------- /test/expect/test__flag.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__flag.mli -------------------------------------------------------------------------------- /test/expect/test__help.ml: -------------------------------------------------------------------------------- 1 | module Command = Cmdlang.Command 2 | 3 | let%expect_test "flag" = 4 | let test = 5 | Arg_test.create 6 | (let%map_open.Command hello = Arg.flag [ "print-hello" ] ~doc:"print Hello" in 7 | (ignore (hello : bool) [@coverage off])) 8 | in 9 | Arg_test.eval_all test { prog = "test"; args = [ "--help" ] }; 10 | [%expect 11 | {| 12 | ----------------------------------------------------- Climate 13 | Evaluation Failed: Usage: test [OPTION]… 14 | 15 | Options: 16 | --print-hello print Hello 17 | -h, --help Show this help message. 18 | ----------------------------------------------------- Cmdliner 19 | NAME 20 | test 21 | 22 | SYNOPSIS 23 | test [--print-hello] [OPTION]… 24 | 25 | OPTIONS 26 | --print-hello 27 | print Hello. 28 | 29 | COMMON OPTIONS 30 | --help[=FMT] (default=auto) 31 | Show this help in format FMT. The value FMT must be one of auto, 32 | pager, groff or plain. With auto, the format is pager or plain 33 | whenever the TERM env var is dumb or undefined. 34 | 35 | EXIT STATUS 36 | test exits with: 37 | 38 | 0 on success. 39 | 40 | 123 on indiscriminate errors reported on standard error. 41 | 42 | 124 on command line parsing errors. 43 | ----------------------------------------------------- Core_command 44 | ("Evaluation Failed" ( 45 | "Command.Failed_to_parse_command_line(\"unknown flag --help\")")) 46 | ----------------------------------------------------- Stdlib_runner 47 | Usage: test [OPTIONS] 48 | 49 | eval-stdlib-runner 50 | 51 | Options: 52 | --print-hello print Hello (optional) 53 | -help Display this list of options 54 | --help Display this list of options 55 | 56 | 57 | 125 on unexpected internal errors (bugs). 58 | |}]; 59 | () 60 | ;; 61 | -------------------------------------------------------------------------------- /test/expect/test__help.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__help.mli -------------------------------------------------------------------------------- /test/expect/test__invalid_pos_opt.ml: -------------------------------------------------------------------------------- 1 | (* In this test we characterize what happens when more positional argument 2 | follow an optional positional argument. *) 3 | 4 | module Command = Cmdlang.Command 5 | 6 | let test = 7 | let%map_open.Command a = Arg.pos_opt ~pos:0 Param.string ~doc:"value for a" 8 | and b = Arg.pos ~pos:1 Param.string ~doc:"value for b" in 9 | print_s [%sexp { a : string option; b : string }] 10 | ;; 11 | 12 | let%expect_test "invalid_pos_sequence" = 13 | let test = Arg_test.create test in 14 | Arg_test.eval_all test { prog = "test"; args = [ "A"; "B" ] }; 15 | [%expect 16 | {| 17 | ----------------------------------------------------- Climate 18 | ((a (A)) (b B)) 19 | ----------------------------------------------------- Cmdliner 20 | ((a (A)) (b B)) 21 | ----------------------------------------------------- Core_command 22 | ((a (A)) (b B)) 23 | ----------------------------------------------------- Stdlib_runner 24 | ((a (A)) (b B)) 25 | |}]; 26 | Arg_test.eval_all test { prog = "test"; args = [ "B" ] }; 27 | [%expect 28 | {| 29 | ----------------------------------------------------- Climate 30 | Evaluation Failed: Missing required positional argument at position 1. 31 | ----------------------------------------------------- Cmdliner 32 | test: required argument STRING is missing 33 | Usage: test [OPTION]… [STRING] STRING 34 | Try 'test --help' for more information. 35 | ("Evaluation Failed" ((exit_code 124))) 36 | ----------------------------------------------------- Core_command 37 | ("Evaluation Failed" "missing anonymous argument: STRING") 38 | ----------------------------------------------------- Stdlib_runner 39 | Missing required positional argument at position 1. 40 | ("Evaluation Failed" ((exit_code 2))) 41 | |}]; 42 | () 43 | ;; 44 | 45 | (* When converting the spec to a command, core.command rejects it. In climate 46 | and cmdliner, the spec is successfully translated, however it will fail when 47 | the optional positional argument isn't supplied. *) 48 | 49 | let cmd = Command.make ~summary:"test" test 50 | 51 | let%expect_test "base" = 52 | require_does_raise [%here] (fun () -> Cmdlang_to_base.Translate.command_unit cmd); 53 | [%expect 54 | {| 55 | (Failure 56 | "the grammar [STRING] STRING for anonymous arguments is not supported because there is the possibility for arguments (STRING) following a variable number of arguments ([STRING]). Supporting such grammars would complicate the implementation significantly.") 57 | |}]; 58 | () 59 | ;; 60 | 61 | let%expect_test "climate" = 62 | let cmd = Cmdlang_to_climate.Translate.command cmd in 63 | let run args = 64 | match Climate.For_test.eval_result ~program_name:"./main.exe" cmd args with 65 | | Ok () -> () 66 | | Error e -> 67 | print_string "Evaluation Failed: "; 68 | Climate_non_ret.print e 69 | | exception e -> print_s [%sexp "Evaluation Raised", (e : Exn.t)] [@coverage off] 70 | in 71 | run [ "A"; "B" ]; 72 | [%expect {| ((a (A)) (b B)) |}]; 73 | run [ "B" ]; 74 | [%expect {| Evaluation Failed: Missing required positional argument at position 1. |}]; 75 | () 76 | ;; 77 | 78 | let%expect_test "cmdliner" = 79 | let cmd = Cmdlang_to_cmdliner.Translate.command cmd ~name:"./main.exe" in 80 | let run args = 81 | Cmdliner.Cmd.eval cmd ~argv:(Array.concat [ [| "./main.exe" |]; Array.of_list args ]) 82 | |> Stdlib.print_int 83 | in 84 | run [ "A"; "B" ]; 85 | [%expect 86 | {| 87 | ((a (A)) (b B)) 88 | 0 89 | |}]; 90 | run [ "B" ]; 91 | [%expect 92 | {| 93 | ./main.exe: required argument STRING is missing 94 | Usage: ./main.exe [OPTION]… [STRING] STRING 95 | Try './main.exe --help' for more information. 96 | 124 97 | |}]; 98 | () 99 | ;; 100 | 101 | let%expect_test "stdlib-runner" = 102 | let run args = 103 | Cmdlang_stdlib_runner.eval_exit_code 104 | cmd 105 | ~argv:(Array.concat [ [| "./main.exe" |]; Array.of_list args ]) 106 | |> Stdlib.print_int 107 | in 108 | run [ "A"; "B" ]; 109 | [%expect 110 | {| 111 | ((a (A)) (b B)) 112 | 0 113 | |}]; 114 | run [ "B" ]; 115 | [%expect 116 | {| 117 | Missing required positional argument at position 1. 118 | 2 119 | |}]; 120 | () 121 | ;; 122 | -------------------------------------------------------------------------------- /test/expect/test__invalid_pos_opt.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__invalid_pos_opt.mli -------------------------------------------------------------------------------- /test/expect/test__named.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__named.mli -------------------------------------------------------------------------------- /test/expect/test__negative_int_args.ml: -------------------------------------------------------------------------------- 1 | module Command = Cmdlang.Command 2 | 3 | let%expect_test "negative positional" = 4 | let test = 5 | Arg_test.create 6 | (let%map_open.Command string = 7 | Arg.pos ~pos:0 Param.int ~doc:"an integer" 8 | |> Arg.map ~f:(fun i -> 9 | match Ordering.of_int i with 10 | | Less -> ("negative" [@coverage off]) 11 | | Equal -> "zero" 12 | | Greater -> "positive") 13 | in 14 | print_endline string) 15 | in 16 | Arg_test.eval_all test { prog = "test"; args = [ "0" ] }; 17 | [%expect 18 | {| 19 | ----------------------------------------------------- Climate 20 | zero 21 | ----------------------------------------------------- Cmdliner 22 | zero 23 | ----------------------------------------------------- Core_command 24 | zero 25 | ----------------------------------------------------- Stdlib_runner 26 | zero 27 | |}]; 28 | Arg_test.eval_all test { prog = "test"; args = [ "+1" ] }; 29 | [%expect 30 | {| 31 | ----------------------------------------------------- Climate 32 | positive 33 | ----------------------------------------------------- Cmdliner 34 | positive 35 | ----------------------------------------------------- Core_command 36 | positive 37 | ----------------------------------------------------- Stdlib_runner 38 | positive 39 | |}]; 40 | (* All backends agree, negative numbers are not supported as positional 41 | arguments, because they look like flags. *) 42 | Arg_test.eval_all test { prog = "test"; args = [ "-1" ] }; 43 | [%expect 44 | {| 45 | ----------------------------------------------------- Climate 46 | Evaluation Failed: Unknown argument name: -1 47 | ----------------------------------------------------- Cmdliner 48 | test: unknown option '-1'. 49 | Usage: test [OPTION]… INT 50 | Try 'test --help' for more information. 51 | ("Evaluation Failed" ((exit_code 124))) 52 | ----------------------------------------------------- Core_command 53 | ("Evaluation Failed" ( 54 | "Command.Failed_to_parse_command_line(\"unknown flag -1\")")) 55 | ----------------------------------------------------- Stdlib_runner 56 | test: unknown option '-1'. 57 | Usage: test [OPTIONS] [ARGUMENTS] 58 | 59 | eval-stdlib-runner 60 | 61 | Arguments: 62 | an integer (required) 63 | 64 | Options: 65 | -help Display this list of options 66 | --help Display this list of options 67 | ("Evaluation Failed" ((exit_code 2))) 68 | |}]; 69 | () 70 | ;; 71 | 72 | let%expect_test "negative named" = 73 | let test = 74 | Arg_test.create 75 | (let%map_open.Command string = 76 | Arg.named [ "n" ] Param.int ~doc:"an integer" 77 | |> Arg.map ~f:(fun i -> 78 | match Ordering.of_int i with 79 | | Less -> "negative" 80 | | Equal -> "zero" 81 | | Greater -> "positive") 82 | in 83 | print_endline string) 84 | in 85 | Arg_test.eval_all test { prog = "test"; args = [ "-n"; "0" ] }; 86 | [%expect 87 | {| 88 | ----------------------------------------------------- Climate 89 | zero 90 | ----------------------------------------------------- Cmdliner 91 | zero 92 | ----------------------------------------------------- Core_command 93 | zero 94 | ----------------------------------------------------- Stdlib_runner 95 | zero 96 | |}]; 97 | Arg_test.eval_all test { prog = "test"; args = [ "-n"; "+1" ] }; 98 | [%expect 99 | {| 100 | ----------------------------------------------------- Climate 101 | positive 102 | ----------------------------------------------------- Cmdliner 103 | positive 104 | ----------------------------------------------------- Core_command 105 | positive 106 | ----------------------------------------------------- Stdlib_runner 107 | positive 108 | |}]; 109 | (* When the arg is named, cmdliner does not support negative values. *) 110 | Arg_test.eval_all test { prog = "test"; args = [ "-n"; "-1" ] }; 111 | [%expect 112 | {| 113 | ----------------------------------------------------- Climate 114 | negative 115 | ----------------------------------------------------- Cmdliner 116 | test: unknown option '-1'. 117 | Usage: test [-n INT] [OPTION]… 118 | Try 'test --help' for more information. 119 | ("Evaluation Failed" ((exit_code 124))) 120 | ----------------------------------------------------- Core_command 121 | negative 122 | ----------------------------------------------------- Stdlib_runner 123 | negative 124 | |}]; 125 | () 126 | ;; 127 | -------------------------------------------------------------------------------- /test/expect/test__negative_int_args.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__negative_int_args.mli -------------------------------------------------------------------------------- /test/expect/test__param.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__param.mli -------------------------------------------------------------------------------- /test/expect/test__pos.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbarbin/cmdlang/d25cfad35e538df21c397d7d77e1d07991b44076/test/expect/test__pos.mli --------------------------------------------------------------------------------