diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml new file mode 100644 index 00000000..6ca82301 --- /dev/null +++ b/.github/workflows/haskell-ci.yml @@ -0,0 +1,273 @@ +# This GitHub workflow config has been generated by a script via +# +# haskell-ci 'github' 'optparse-applicative.cabal' +# +# To regenerate the script (for example after adjusting tested-with) run +# +# haskell-ci regenerate +# +# For more information, see https://github.com/haskell-CI/haskell-ci +# +# version: 0.15.20230321 +# +# REGENDATA ("0.15.20230321",["github","optparse-applicative.cabal"]) +# +name: Haskell-CI +on: + - push + - pull_request +jobs: + linux: + name: Haskell-CI - Linux - ${{ matrix.compiler }} + runs-on: ubuntu-20.04 + timeout-minutes: + 60 + container: + image: buildpack-deps:bionic + continue-on-error: ${{ matrix.allow-failure }} + strategy: + matrix: + include: + - compiler: ghc-9.6.1 + compilerKind: ghc + compilerVersion: 9.6.1 + setup-method: ghcup + allow-failure: false + - compiler: ghc-9.4.4 + compilerKind: ghc + compilerVersion: 9.4.4 + setup-method: ghcup + allow-failure: false + - compiler: ghc-9.2.7 + compilerKind: ghc + compilerVersion: 9.2.7 + setup-method: ghcup + allow-failure: false + - compiler: ghc-9.0.2 + compilerKind: ghc + compilerVersion: 9.0.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.7 + compilerKind: ghc + compilerVersion: 8.10.7 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.8.4 + compilerKind: ghc + compilerVersion: 8.8.4 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-8.6.5 + compilerKind: ghc + compilerVersion: 8.6.5 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-8.4.4 + compilerKind: ghc + compilerVersion: 8.4.4 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-8.2.2 + compilerKind: ghc + compilerVersion: 8.2.2 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-8.0.2 + compilerKind: ghc + compilerVersion: 8.0.2 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-7.10.3 + compilerKind: ghc + compilerVersion: 7.10.3 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-7.8.4 + compilerKind: ghc + compilerVersion: 7.8.4 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-7.6.3 + compilerKind: ghc + compilerVersion: 7.6.3 + setup-method: hvr-ppa + allow-failure: false + - compiler: ghc-7.4.2 + compilerKind: ghc + compilerVersion: 7.4.2 + setup-method: hvr-ppa + allow-failure: false + fail-fast: false + steps: + - name: apt + run: | + apt-get update + apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 + if [ "${{ matrix.setup-method }}" = ghcup ]; then + mkdir -p "$HOME/.ghcup/bin" + curl -sL https://downloads.haskell.org/ghcup/0.1.19.2/x86_64-linux-ghcup-0.1.19.2 > "$HOME/.ghcup/bin/ghcup" + chmod a+x "$HOME/.ghcup/bin/ghcup" + "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) + "$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false) + else + apt-add-repository -y 'ppa:hvr/ghc' + apt-get update + apt-get install -y "$HCNAME" + mkdir -p "$HOME/.ghcup/bin" + curl -sL https://downloads.haskell.org/ghcup/0.1.19.2/x86_64-linux-ghcup-0.1.19.2 > "$HOME/.ghcup/bin/ghcup" + chmod a+x "$HOME/.ghcup/bin/ghcup" + "$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false) + fi + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: Set PATH and environment variables + run: | + echo "$HOME/.cabal/bin" >> $GITHUB_PATH + echo "LANG=C.UTF-8" >> "$GITHUB_ENV" + echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV" + echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV" + HCDIR=/opt/$HCKIND/$HCVER + if [ "${{ matrix.setup-method }}" = ghcup ]; then + HC=$HOME/.ghcup/bin/$HCKIND-$HCVER + echo "HC=$HC" >> "$GITHUB_ENV" + echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV" + echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV" + echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV" + else + HC=$HCDIR/bin/$HCKIND + echo "HC=$HC" >> "$GITHUB_ENV" + echo "HCPKG=$HCDIR/bin/$HCKIND-pkg" >> "$GITHUB_ENV" + echo "HADDOCK=$HCDIR/bin/haddock" >> "$GITHUB_ENV" + echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV" + fi + + HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') + echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" + echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" + echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV" + echo "HEADHACKAGE=false" >> "$GITHUB_ENV" + echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV" + echo "GHCJSARITH=0" >> "$GITHUB_ENV" + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: env + run: | + env + - name: write cabal config + run: | + mkdir -p $CABAL_DIR + cat >> $CABAL_CONFIG <> $CABAL_CONFIG < cabal-plan.xz + echo 'de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc cabal-plan.xz' | sha256sum -c - + xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan + rm -f cabal-plan.xz + chmod a+x $HOME/.cabal/bin/cabal-plan + cabal-plan --version + - name: checkout + uses: actions/checkout@v3 + with: + path: source + - name: initial cabal.project for sdist + run: | + touch cabal.project + echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project + cat cabal.project + - name: sdist + run: | + mkdir -p sdist + $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist + - name: unpack + run: | + mkdir -p unpacked + find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \; + - name: generate cabal.project + run: | + PKGDIR_optparse_applicative="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/optparse-applicative-[0-9.]*')" + echo "PKGDIR_optparse_applicative=${PKGDIR_optparse_applicative}" >> "$GITHUB_ENV" + rm -f cabal.project cabal.project.local + touch cabal.project + touch cabal.project.local + echo "packages: ${PKGDIR_optparse_applicative}" >> cabal.project + if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo "package optparse-applicative" >> cabal.project ; fi + if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi + cat >> cabal.project <> cabal.project.local + cat cabal.project + cat cabal.project.local + - name: dump install plan + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all + cabal-plan + - name: restore cache + uses: actions/cache/restore@v3 + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store + restore-keys: ${{ runner.os }}-${{ matrix.compiler }}- + - name: install dependencies + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all + - name: build w/o tests + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: build + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always + - name: tests + run: | + $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct + - name: cabal check + run: | + cd ${PKGDIR_optparse_applicative} || false + ${CABAL} -vnormal check + - name: haddock + run: | + $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + - name: unconstrained build + run: | + rm -f cabal.project.local + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: save cache + uses: actions/cache/save@v3 + if: always() + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9260e707..00000000 --- a/.travis.yml +++ /dev/null @@ -1,180 +0,0 @@ -# This Travis job script has been generated by a script via -# -# haskell-ci '--ghc-head' '--hlint' '--hlint-yaml=.hlint.yaml' 'optparse-applicative.cabal' -# -# For more information, see https://github.com/haskell-CI/haskell-ci -# -# version: 0.3.20190429 -# -language: c -dist: xenial -git: - # whether to recursively clone submodules - submodules: false -cache: - directories: - - $HOME/.cabal/packages - - $HOME/.cabal/store -before_cache: - - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log - # remove files that are regenerated by 'cabal update' - - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.* - - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json - - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache - - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar - - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx - - rm -rfv $CABALHOME/packages/head.hackage -matrix: - include: - - compiler: ghc-8.10.1 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-8.10.1","cabal-install-3.0"]}} - env: GHCHEAD=true - - compiler: ghc-8.8.1 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-8.8.1","cabal-install-3.0"]}} - - compiler: ghc-8.6.5 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-8.6.5","cabal-install-2.4"]}} - - compiler: ghc-8.4.4 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-8.4.4","cabal-install-2.4"]}} - - compiler: ghc-8.2.2 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-8.2.2","cabal-install-2.4"]}} - - compiler: ghc-8.0.2 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-8.0.2","cabal-install-2.4"]}} - - compiler: ghc-7.10.3 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-7.10.3","cabal-install-2.4"]}} - - compiler: ghc-7.8.4 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-7.8.4","cabal-install-2.4"]}} - - compiler: ghc-7.6.3 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-7.6.3","cabal-install-2.4"]}} - - compiler: ghc-7.4.2 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-7.4.2","cabal-install-2.4"]}} - - compiler: ghc-7.2.2 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-7.2.2","cabal-install-2.4"]}} - - compiler: ghc-7.0.4 - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-7.0.4","cabal-install-2.4"]}} - - compiler: ghc-head - addons: {"apt":{"sources":["hvr-ghc"],"packages":["ghc-head","cabal-install-head"]}} - env: GHCHEAD=true - allow_failures: - - compiler: ghc-head -before_install: - - HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//') - - HCPKG="$HC-pkg" - - unset CC - - CABAL=/opt/ghc/bin/cabal - - CABALHOME=$HOME/.cabal - - export PATH="$CABALHOME/bin:$PATH" - - TOP=$(pwd) - - HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') )) - - echo $HCNUMVER - - CABAL="$CABAL -vnormal+nowrap+markoutput" - - set -o pipefail - - | - echo 'function blue(s) { printf "\033[0;34m" s "\033[0m " }' >> .colorful.awk - echo 'BEGIN { state = "output"; }' >> .colorful.awk - echo '/^-----BEGIN CABAL OUTPUT-----$/ { state = "cabal" }' >> .colorful.awk - echo '/^-----END CABAL OUTPUT-----$/ { state = "output" }' >> .colorful.awk - echo '!/^(-----BEGIN CABAL OUTPUT-----|-----END CABAL OUTPUT-----)/ {' >> .colorful.awk - echo ' if (state == "cabal") {' >> .colorful.awk - echo ' print blue($0)' >> .colorful.awk - echo ' } else {' >> .colorful.awk - echo ' print $0' >> .colorful.awk - echo ' }' >> .colorful.awk - echo '}' >> .colorful.awk - - cat .colorful.awk - - | - color_cabal_output () { - awk -f $TOP/.colorful.awk - } - - echo text | color_cabal_output -install: - - ${CABAL} --version - - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]" - - TEST=--enable-tests - - BENCH=--enable-benchmarks - - GHCHEAD=${GHCHEAD-false} - - rm -f $CABALHOME/config - - | - echo "verbose: normal +nowrap +markoutput" >> $CABALHOME/config - echo "remote-build-reporting: anonymous" >> $CABALHOME/config - echo "remote-repo-cache: $CABALHOME/packages" >> $CABALHOME/config - echo "logs-dir: $CABALHOME/logs" >> $CABALHOME/config - echo "world-file: $CABALHOME/world" >> $CABALHOME/config - echo "extra-prog-path: $CABALHOME/bin" >> $CABALHOME/config - echo "symlink-bindir: $CABALHOME/bin" >> $CABALHOME/config - echo "installdir: $CABALHOME/bin" >> $CABALHOME/config - echo "build-summary: $CABALHOME/logs/build.log" >> $CABALHOME/config - echo "store-dir: $CABALHOME/store" >> $CABALHOME/config - echo "install-dirs user" >> $CABALHOME/config - echo " prefix: $CABALHOME" >> $CABALHOME/config - echo "repository hackage.haskell.org" >> $CABALHOME/config - echo " url: http://hackage.haskell.org/" >> $CABALHOME/config - - | - if $GHCHEAD; then - echo "allow-newer: $($HCPKG list --simple-output | sed -E 's/([a-zA-Z-]+)-[0-9.]+/*:\1/g')" >> $CABALHOME/config - - echo "repository head.hackage" >> $CABALHOME/config - echo " url: http://head.hackage.haskell.org/" >> $CABALHOME/config - echo " secure: True" >> $CABALHOME/config - echo " root-keys: 07c59cb65787dedfaef5bd5f987ceb5f7e5ebf88b904bbd4c5cbdeb2ff71b740" >> $CABALHOME/config - echo " 2e8555dde16ebd8df076f1a8ef13b8f14c66bad8eafefd7d9e37d0ed711821fb" >> $CABALHOME/config - echo " 8f79fd2389ab2967354407ec852cbe73f2e8635793ac446d09461ffb99527f6e" >> $CABALHOME/config - echo " key-threshold: 3" >> $CABALHOME/config - fi - - cat $CABALHOME/config - - rm -fv cabal.project cabal.project.local cabal.project.freeze - - travis_retry ${CABAL} v2-update -v - - if [ $HCNUMVER -eq 80605 ] ; then ${CABAL} v2-install -w ${HC} -j2 hlint --constraint='hlint ==2.1.*' | color_cabal_output ; fi - # Generate cabal.project - - rm -rf cabal.project cabal.project.local cabal.project.freeze - - touch cabal.project - - | - echo 'packages: "."' >> cabal.project - - | - echo "write-ghc-environment-files: always" >> cabal.project - - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(optparse-applicative)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done" - - cat cabal.project || true - - cat cabal.project.local || true - - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi - - ${CABAL} v2-freeze -w ${HC} ${TEST} ${BENCH} | color_cabal_output - - "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'" - - rm cabal.project.freeze - - ${CABAL} v2-build -w ${HC} ${TEST} ${BENCH} --dep -j2 all | color_cabal_output - - ${CABAL} v2-build -w ${HC} --disable-tests --disable-benchmarks --dep -j2 all | color_cabal_output -script: - - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX) - # Packaging... - - ${CABAL} v2-sdist all | color_cabal_output - # Unpacking... - - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/ - - cd ${DISTDIR} || false - - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \; - # Generate cabal.project - - rm -rf cabal.project cabal.project.local cabal.project.freeze - - touch cabal.project - - | - echo 'packages: "optparse-applicative-*/*.cabal"' >> cabal.project - - | - echo "write-ghc-environment-files: always" >> cabal.project - - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(optparse-applicative)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done" - - cat cabal.project || true - - cat cabal.project.local || true - # Building... - # this builds all libraries and executables (without tests/benchmarks) - - ${CABAL} v2-build -w ${HC} --disable-tests --disable-benchmarks all | color_cabal_output - # Building with tests and benchmarks... - # build & run tests, build benchmarks - - ${CABAL} v2-build -w ${HC} ${TEST} ${BENCH} all | color_cabal_output - # Testing... - - ${CABAL} v2-test -w ${HC} ${TEST} ${BENCH} all | color_cabal_output - # HLint.. - - if [ $HCNUMVER -eq 80605 ] ; then (cd optparse-applicative-* && hlint -h ${TOP}/.hlint.yaml src) ; fi - # cabal check... - - (cd optparse-applicative-* && ${CABAL} -vnormal check) - # haddock... - - ${CABAL} v2-haddock -w ${HC} ${TEST} ${BENCH} all | color_cabal_output - # Building without installed constraints for packages in global-db... - - rm -f cabal.project.local - - ${CABAL} v2-build -w ${HC} --disable-tests --disable-benchmarks all | color_cabal_output - -# REGENDATA ["--ghc-head","--hlint","--hlint-yaml=.hlint.yaml","optparse-applicative.cabal"] -# EOF diff --git a/CHANGELOG.md b/CHANGELOG.md index ab20930a..dadbd9f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,70 @@ -## Unreleased - -- Make tabulation width configurable in usage texts. +## Next - Add `mkCompleterWithOptions`, allowing completers to request that no space is added after the completion. This is useful in situations where not all completions can be computed efficiently, or when they are too many. +## Version 0.18.1.0 (29 May 2023) + +- Change pretty printer layout algorithm used. + + The layoutSmart algorithm appears to be extremely slow with some command line + sets, to the point where the program appears to hang. + + Fixes issues: + * \# 476 - Stack executable 'hangs' with 0.17.1 and 0.18.0. + +- Render help text with `AnsiStyle` aware rendering functions. + +## Version 0.18.0.0 (22 May 2023) + +- Move to 'prettyprinter` library for pretty printing. + + This is a potentially breaking change when one uses the '*Doc' family of functions + (like `headerDoc`) from `Options.Applicative`. However, as versions of + 'ansi-wl-pprint > 1.0' export a compatible `Doc` type, this can be mitigated by + using a recent version. + + One can also either import directly from `Options.Applicative.Help` or from the + `Prettyprinter` module of 'prettyprinter'. + +- Allow commands to be disambiguated in a similar manner to flags when the + `disambiguate` modifier is used. + + This is a potentially breaking change as the internal `CmdReader` constructor + has been adapted so it is able to be inspected to a greater degree to support + finding prefix matches. + +## Version 0.17.1.0 (22 May 2023) + +- Widen bounds for `ansi-wl-pprint`. This supports the use of `prettyprinter` + in a non-breaking way, as the `ansi-wl-pprint > 1.0` support the newer + library. + +- Export `helpIndent` from `Options.Applicative`. + +- Export completion script generators from `Options.Applicative.BashCompletion`. + +- Add `simpleVersioner` utility for adding a '--version' option to a parser. + +- Improve documentation. + +- Drop support for GHC 7.0 and 7.2. + +## Version 0.17.0.0 (1 Feb 2022) + +- Make tabulation width configurable in usage texts. + +- Separate program name and description in ParserHelp type. + +- Add `helperWith` function, which can be easily used to + localize the help flag. + +- Improve usage texts when command names are long. + +- Improve Documentation. + ## Version 0.16.1.0 (21 Nov 2020) - Guard `process` dependency behind an on by default flag. diff --git a/README.md b/README.md index 415676cb..8c61e555 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # optparse-applicative [![Continuous Integration status][status-png]][status] -[![Hackage matrix][hackage-matrix-png]][hackage-matrix] [![Hackage page (downloads and API reference)][hackage-png]][hackage] [![Hackage-Deps][hackage-deps-png]][hackage-deps] @@ -75,7 +74,6 @@ Here's a simple example of a parser. ```haskell import Options.Applicative -import Data.Semigroup ((<>)) data Sample = Sample { hello :: String @@ -303,7 +301,14 @@ Having `Applicative` and `Alternative` instances, optparse-applicative parsers are also able to be composed with standard combinators. For example: `optional :: Alternative f => f a -> f (Maybe a)` will mean the user is not required to provide input for the affected -`Parser`. +`Parser`. For example, the following parser will return `Nothing` +instead of failing if the user doesn't supply an `output` option: + +```haskell +optional $ strOption + ( long "output" + <> metavar "DIRECTORY" ) +``` ### Running parsers @@ -504,7 +509,7 @@ number of arguments, combine the `argument` builder with either the `many` or `some` combinator: ```haskell -some (argument str (metavar "FILES...")) +some (argument str (metavar "FILES")) ``` Note that arguments starting with `-` are considered options by @@ -527,22 +532,6 @@ functions, each with its own set of options, and possibly some global options that apply to all of them. Typical examples are version control systems like `git`, or build tools like `cabal`. -A command can be created using the `subparser` builder (or `hsubparser`, -which is identical but for an additional `--help` option on each -command), and commands can be added with the `command` modifier. -For example - -```haskell -subparser - ( command "add" (info addOptions ( progDesc "Add a file to the repository" )) - <> command "commit" (info commitOptions ( progDesc "Record changes to the repository" )) - ) -``` - -Each command takes a full `ParserInfo` structure, which will be -used to extract a description for this command when generating a -help text. - Note that all the parsers appearing in a command need to have the same type. For this reason, it is often best to use a sum type which has the same structure as the command itself. For example, @@ -559,6 +548,22 @@ data Command ... ``` +A command can then be created using the `subparser` builder (or +`hsubparser`, which is identical but for an additional `--help` option +on each command), and commands can be added with the `command` +modifier. For example, + +```haskell +subparser + ( command "add" (info addCommand ( progDesc "Add a file to the repository" )) + <> command "commit" (info commitCommand ( progDesc "Record changes to the repository" )) + ) +``` + +Each command takes a full `ParserInfo` structure, which will be +used to extract a description for this command when generating a +help text. + Alternatively, you can directly return an `IO` action from a parser, and execute it using `join` from `Control.Monad`. @@ -702,6 +707,11 @@ main = customExecParser p opts ``` +**Note**. If an option name is a prefix of another option, then it +will never be matched when disambiguation is on. See +[#419](https://github.com/pcapriotti/optparse-applicative/issues/419) +for more details. + ### Customising the help screen optparse-applicative has a number of combinators to help customise @@ -711,8 +721,8 @@ The `progDesc`, `header`, and `footer` functions can be used to specify a brief description or tagline for the program, and detailed information surrounding the generated option and command descriptions. -Internally we actually use the [ansi-wl-pprint][ansi-wl-pprint] -library, and one can use the `headerDoc` combinator and friends if +Internally we actually use the [prettyprinter][prettyprinter] +library, and one can supply either text or prettyprinter `Doc` elements if additional customisation is required. To display the usage text, the user may type `--help` if the `helper` @@ -1013,13 +1023,11 @@ simplified implementation. [blog]: http://paolocapriotti.com/blog/2012/04/27/applicative-option-parser/ [hackage]: http://hackage.haskell.org/package/optparse-applicative [hackage-png]: http://img.shields.io/hackage/v/optparse-applicative.svg - [hackage-matrix]: https://matrix.hackage.haskell.org/package/optparse-applicative - [hackage-matrix-png]: https://matrix.hackage.haskell.org/api/v2/packages/optparse-applicative/badge [hackage-deps]: http://packdeps.haskellers.com/reverse/optparse-applicative [hackage-deps-png]: https://img.shields.io/hackage-deps/v/optparse-applicative.svg [monoid]: http://hackage.haskell.org/package/base/docs/Data-Monoid.html [semigroup]: http://hackage.haskell.org/package/base/docs/Data-Semigroup.html [parsec]: http://hackage.haskell.org/package/parsec - [status]: http://travis-ci.org/pcapriotti/optparse-applicative?branch=master - [status-png]: https://api.travis-ci.org/pcapriotti/optparse-applicative.svg?branch=master - [ansi-wl-pprint]: http://hackage.haskell.org/package/ansi-wl-pprint + [status]: https://github.com/pcapriotti/optparse-applicative/actions/workflows/haskell-ci.yml + [status-png]: https://github.com/pcapriotti/optparse-applicative/workflows/Haskell-CI/badge.svg + [prettyprinter]: http://hackage.haskell.org/package/prettyprinter diff --git a/hercules-ci-optparse-applicative.cabal b/hercules-ci-optparse-applicative.cabal index 6406c988..7f155455 100644 --- a/hercules-ci-optparse-applicative.cabal +++ b/hercules-ci-optparse-applicative.cabal @@ -1,5 +1,5 @@ name: hercules-ci-optparse-applicative -version: 0.16.1.0 +version: 0.18.1.0 synopsis: Utilities and combinators for parsing command line options (fork) description: optparse-applicative is a haskell library for parsing options @@ -36,6 +36,7 @@ extra-source-files: CHANGELOG.md tests/helponemptysub.err.txt tests/long_equals.err.txt tests/formatting.err.txt + tests/formatting-long-subcommand.err.txt tests/nested.err.txt tests/optional.err.txt tests/nested_optional.err.txt @@ -44,17 +45,22 @@ extra-source-files: CHANGELOG.md homepage: https://github.com/hercules-ci/optparse-applicative bug-reports: https://github.com/hercules-ci/optparse-applicative/issues tested-with: - GHC==7.0.4, - GHC==7.2.2, - GHC==7.4.2, - GHC==7.6.3, - GHC==7.8.4, - GHC==7.10.3, - GHC==8.0.2, - GHC==8.2.2, - GHC==8.4.4, - GHC==8.6.5, - GHC==8.8.1 + GHC==9.6.1 + GHC==9.4.4 + GHC==9.2.7 + GHC==9.0.2 + GHC==8.10.7 + GHC==8.8.4 + GHC==8.6.5 + GHC==8.4.4 + GHC==8.2.2 + GHC==8.0.2 + GHC==7.10.3 + GHC==7.8.4 + GHC==7.6.3 + GHC==7.4.2 + GHC==7.2.2 + GHC==7.0.4 source-repository head type: git @@ -94,16 +100,18 @@ library , Options.Applicative.Types , Options.Applicative.Internal - build-depends: base == 4.* + build-depends: base >= 4.5 && < 5 + , text >= 1.2 , transformers >= 0.2 && < 0.7 , transformers-compat >= 0.3 && < 0.8 - , ansi-wl-pprint >= 0.6.8 && < 0.7 + , prettyprinter >= 1.7 && < 1.8 + , prettyprinter-ansi-terminal >= 1.1 && < 1.2 if flag(process) build-depends: process >= 1.0 && < 1.7 if !impl(ghc >= 8) - build-depends: semigroups >= 0.10 && < 0.20 + build-depends: semigroups >= 0.10 && < 0.21 , fail == 4.9.* test-suite tests @@ -122,6 +130,7 @@ test-suite tests , Examples.Commands , Examples.Formatting , Examples.Hello + , Examples.LongSub build-depends: base , hercules-ci-optparse-applicative diff --git a/src/Options/Applicative.hs b/src/Options/Applicative.hs index e7bf0b06..b6232d32 100644 --- a/src/Options/Applicative.hs +++ b/src/Options/Applicative.hs @@ -76,7 +76,7 @@ module Options.Applicative ( abortOption, infoOption, helper, - helperWith, + simpleVersioner, -- ** Modifiers -- @@ -204,6 +204,7 @@ module Options.Applicative ( columns, helpLongEquals, helpShowGlobals, + helpIndent, defaultPrefs, -- * Completions diff --git a/src/Options/Applicative/BashCompletion.hs b/src/Options/Applicative/BashCompletion.hs index 052ed4e8..eca2553f 100644 --- a/src/Options/Applicative/BashCompletion.hs +++ b/src/Options/Applicative/BashCompletion.hs @@ -6,7 +6,11 @@ -- -- for more information on bash completion. module Options.Applicative.BashCompletion - ( bashCompletionParser + ( bashCompletionParser, + + bashCompletionScript, + fishCompletionScript, + zshCompletionScript, ) where import Control.Applicative @@ -52,14 +56,15 @@ data Richness bashCompletionParser :: ParserInfo a -> ParserPrefs -> Parser CompletionResult bashCompletionParser pinfo pprefs = complParser where - failure opts = CompletionResult - { execCompletion = \progn -> unlines <$> opts progn } + returnCompletions opts = + CompletionResult $ + \progn -> unlines <$> opts progn featuresParser :: Parser Features featuresParser = Features <$> richnessParser <*> protocolVersionParser protocolVersionParser :: Parser Int - protocolVersionParser = option auto (long "optparse-completion-version" <> value 0) + protocolVersionParser = option auto (long "optparse-completion-version" `mappend` value 0) richnessParser :: Parser Richness richnessParser = @@ -68,8 +73,11 @@ bashCompletionParser pinfo pprefs = complParser <*> option auto (long "bash-completion-command-desc-length" `mappend` internal `mappend` value 40) <|> pure Standard + scriptRequest = + CompletionResult . fmap pure + complParser = asum - [ failure <$> + [ returnCompletions <$> ( bashCompletionQuery pinfo pprefs -- To get rich completions, one just needs the first -- command. To customise the lengths, use either of @@ -81,15 +89,13 @@ bashCompletionParser pinfo pprefs = complParser `mappend` internal) <*> option auto (long "bash-completion-index" `mappend` internal) ) - , failure <$> - (bashCompletionScript <$> - strOption (long "bash-completion-script" `mappend` internal)) - , failure <$> - (fishCompletionScript <$> - strOption (long "fish-completion-script" `mappend` internal)) - , failure <$> - (zshCompletionScript <$> - strOption (long "zsh-completion-script" `mappend` internal)) + + , scriptRequest . bashCompletionScript <$> + strOption (long "bash-completion-script" `mappend` internal) + , scriptRequest . fishCompletionScript <$> + strOption (long "fish-completion-script" `mappend` internal) + , scriptRequest . zshCompletionScript <$> + strOption (long "zsh-completion-script" `mappend` internal) ] bashCompletionQuery :: ParserInfo a -> ParserPrefs -> Features -> [String] -> Int -> String -> IO [String] @@ -142,11 +148,11 @@ bashCompletionQuery pinfo pprefs features ws i _ = case runCompletion compl ppre -> return [] | otherwise -> run_completer (crCompleter rdr) - CmdReader _ ns p + CmdReader _ ns | argumentIsUnreachable reachability -> return [] | otherwise - -> return . fmap legacyCompletionItem . add_cmd_help p $ filter_names ns + -> return . fmap legacyCompletionItem . with_cmd_help $ filter (is_completion . fst) ns -- When doing enriched completions, add any help specified -- to the completion variables (tab separated). @@ -161,30 +167,28 @@ bashCompletionQuery pinfo pprefs features ws i _ = case runCompletion compl ppre -- When doing enriched completions, add the command description -- to the completion variables (tab separated). - add_cmd_help :: Functor f => (String -> Maybe (ParserInfo a)) -> f String -> f String - add_cmd_help p = case richness features of - Standard -> - id - Enriched _ len -> - fmap $ \cmd -> - let h = p cmd >>= unChunk . infoProgDesc - in maybe cmd (\h' -> cmd ++ "\t" ++ render_line len h') h + with_cmd_help :: Functor f => f (String, ParserInfo a) -> f String + with_cmd_help = + case richness features of + Standard -> + fmap fst + Enriched _ len -> + fmap $ \(cmd, cmdInfo) -> + let h = unChunk (infoProgDesc cmdInfo) + in maybe cmd (\h' -> cmd ++ "\t" ++ render_line len h') h show_names :: [OptName] -> [String] - show_names = filter_names . map showOption + show_names = filter is_completion . map showOption -- We only want to show a single line in the completion results description. -- If there was a line break, it would come across as a different completion -- possibility. render_line :: Int -> Doc -> String - render_line len doc = case lines (displayS (renderPretty 1 len doc) "") of + render_line len doc = case lines (prettyString 1 len doc) of [] -> "" [x] -> x x : _ -> x ++ "..." - filter_names :: [String] -> [String] - filter_names = filter is_completion - run_completer :: Completer -> IO [CompletionItem] run_completer c = runCompleter c (fromMaybe "" (listToMaybe ws'')) @@ -207,8 +211,9 @@ bashCompletionQuery pinfo pprefs features ws i _ = case runCompletion compl ppre ++ [ "%files" | cioFiles opts ] ++ ["%value", val] -bashCompletionScript :: String -> String -> IO [String] -bashCompletionScript prog progn = return +-- | Generated bash shell completion script +bashCompletionScript :: String -> String -> String +bashCompletionScript prog progn = unlines -- compopt: see complete -o at https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html [ "_" ++ progn ++ "()" , "{" @@ -263,8 +268,10 @@ words. Tab characters separate items from descriptions. -} -fishCompletionScript :: String -> String -> IO [String] -fishCompletionScript prog progn = return + +-- | Generated fish shell completion script +fishCompletionScript :: String -> String -> String +fishCompletionScript prog progn = unlines [ " function _" ++ progn , " set -l cl (commandline --tokenize --current-process)" , " # Hack around fish issue #3934" @@ -298,8 +305,9 @@ fishCompletionScript prog progn = return , "complete --no-files --command " ++ progn ++ " --arguments '(_" ++ progn ++ ")'" ] -zshCompletionScript :: String -> String -> IO [String] -zshCompletionScript prog progn = return +-- | Generated zsh shell completion script +zshCompletionScript :: String -> String -> String +zshCompletionScript prog progn = unlines -- compadd: http://zsh.sourceforge.net/Doc/Release/Completion-Widgets.html#Completion-Builtin-Commands [ "#compdef " ++ progn , "" diff --git a/src/Options/Applicative/Builder.hs b/src/Options/Applicative/Builder.hs index 1fdf0095..42e40962 100644 --- a/src/Options/Applicative/Builder.hs +++ b/src/Options/Applicative/Builder.hs @@ -195,7 +195,7 @@ showDefault = showDefaultWith show help :: String -> Mod f a help s = optionMod $ \p -> p { propHelp = paragraph s } --- | Specify the help text for an option as a 'Text.PrettyPrint.ANSI.Leijen.Doc' +-- | Specify the help text for an option as a 'Prettyprinter.Doc AnsiStyle' -- value. helpDoc :: Maybe Doc -> Mod f a helpDoc doc = optionMod $ \p -> p { propHelp = Chunk doc } @@ -228,11 +228,11 @@ hidden = optionMod $ \p -> -- | Apply a function to the option description in the usage text. -- -- > import Options.Applicative.Help --- > flag' () (short 't' <> style bold) +-- > flag' () (short 't' <> style (annotate bold)) -- -- /NOTE/: This builder is more flexible than its name and example -- allude. One of the motivating examples for its addition was to --- used `const` to completely replace the usage text of an option. +-- use `const` to completely replace the usage text of an option. style :: ( Doc -> Doc ) -> Mod f a style x = optionMod $ \p -> p { propDescMod = Just x } @@ -294,12 +294,17 @@ completer2 f = fieldMod $ modCompleter2 (`mappend` f) -- | Builder for a command parser. The 'command' modifier can be used to -- specify individual commands. +-- +-- By default, sub-parsers allow backtracking to their parent's options when +-- they are completed. To allow full mixing of parent and sub-parser options, +-- turn on 'subparserInline'; otherwise, to disable backtracking completely, +-- use 'noBacktrack'. subparser :: Mod CommandFields a -> Parser a subparser m = mkParser d g rdr where Mod _ d g = metavar "COMMAND" `mappend` m - (groupName, cmds, subs) = mkCommand m - rdr = CmdReader groupName cmds subs + (groupName, cmds) = mkCommand m + rdr = CmdReader groupName cmds -- | Builder for an argument parser. argument :: ReadM a -> Mod ArgumentFields a -> Parser a @@ -422,7 +427,7 @@ instance Monoid (InfoMod a) where instance Semigroup (InfoMod a) where m1 <> m2 = InfoMod $ applyInfoMod m2 . applyInfoMod m1 --- | Show a full description in the help text of this parser. +-- | Show a full description in the help text of this parser (default). fullDesc :: InfoMod a fullDesc = InfoMod $ \i -> i { infoFullDesc = True } @@ -434,7 +439,7 @@ briefDesc = InfoMod $ \i -> i { infoFullDesc = False } header :: String -> InfoMod a header s = InfoMod $ \i -> i { infoHeader = paragraph s } --- | Specify a header for this parser as a 'Text.PrettyPrint.ANSI.Leijen.Doc' +-- | Specify a header for this parser as a 'Prettyprinter.Doc AnsiStyle' -- value. headerDoc :: Maybe Doc -> InfoMod a headerDoc doc = InfoMod $ \i -> i { infoHeader = Chunk doc } @@ -443,7 +448,7 @@ headerDoc doc = InfoMod $ \i -> i { infoHeader = Chunk doc } footer :: String -> InfoMod a footer s = InfoMod $ \i -> i { infoFooter = paragraph s } --- | Specify a footer for this parser as a 'Text.PrettyPrint.ANSI.Leijen.Doc' +-- | Specify a footer for this parser as a 'Prettyprinter.Doc AnsiStyle' -- value. footerDoc :: Maybe Doc -> InfoMod a footerDoc doc = InfoMod $ \i -> i { infoFooter = Chunk doc } @@ -452,7 +457,7 @@ footerDoc doc = InfoMod $ \i -> i { infoFooter = Chunk doc } progDesc :: String -> InfoMod a progDesc s = InfoMod $ \i -> i { infoProgDesc = paragraph s } --- | Specify a short program description as a 'Text.PrettyPrint.ANSI.Leijen.Doc' +-- | Specify a short program description as a 'Prettyprinter.Doc AnsiStyle' -- value. progDescDoc :: Maybe Doc -> InfoMod a progDescDoc doc = InfoMod $ \i -> i { infoProgDesc = Chunk doc } diff --git a/src/Options/Applicative/Builder/Completer.hs b/src/Options/Applicative/Builder/Completer.hs index 0eb92287..264e1842 100644 --- a/src/Options/Applicative/Builder/Completer.hs +++ b/src/Options/Applicative/Builder/Completer.hs @@ -6,15 +6,18 @@ module Options.Applicative.Builder.Completer , listIOCompleter , listCompleter , bashCompleter + + , requote ) where import Control.Applicative import Prelude import Control.Exception (IOException, try) -import Data.List (isPrefixOf,isSuffixOf) -import System.Environment (lookupEnv) +import Data.List (isPrefixOf) #ifdef MIN_VERSION_process +import Data.List (isSuffixOf) import System.Process (readProcess) +import System.Environment (lookupEnv) #endif import Options.Applicative.Types @@ -42,9 +45,6 @@ bashCompleter action = mkCompleter $ \word -> do bash <- getBash result <- tryIO $ readProcess bash ["-c", cmd] "" return . lines . either (const []) id $ result -#else -bashCompleter = const $ Completer $ const $ return [] -#endif -- | Determines the bash executable. Ideally we'd invoke the same bash that -- is currently active. If $SHELL does not seem to be set to a bash executable @@ -59,6 +59,10 @@ getBash = do _ -> "bash" ) +#else +bashCompleter = const $ Completer $ const $ return [] +#endif + tryIO :: IO a -> IO (Either IOException a) tryIO = try diff --git a/src/Options/Applicative/Builder/Internal.hs b/src/Options/Applicative/Builder/Internal.hs index c43b581c..129e90a8 100644 --- a/src/Options/Applicative/Builder/Internal.hs +++ b/src/Options/Applicative/Builder/Internal.hs @@ -167,8 +167,8 @@ baseProps = OptProperties , propShowGlobal = True } -mkCommand :: Mod CommandFields a -> (Maybe String, [String], String -> Maybe (ParserInfo a)) -mkCommand m = (group, map fst cmds, (`lookup` cmds)) +mkCommand :: Mod CommandFields a -> (Maybe String, [(String, ParserInfo a)]) +mkCommand m = (group, cmds) where Mod f _ _ = m CommandFields cmds group = f (CommandFields [] Nothing) diff --git a/src/Options/Applicative/Common.hs b/src/Options/Applicative/Common.hs index 8d39678d..58852966 100644 --- a/src/Options/Applicative/Common.hs +++ b/src/Options/Applicative/Common.hs @@ -186,20 +186,24 @@ searchArg prefs arg = searchParser $ \opt -> do when (isArg (optMain opt)) cut case optMain opt of - CmdReader _ _ f -> - case (f arg, prefBacktrack prefs) of - (Just subp, NoBacktrack) -> lift $ do + CmdReader _ cs -> do + let + cmdMatches _ + | prefDisambiguate prefs = snd <$> filter (isPrefixOf arg . fst) cs + | otherwise = maybeToList (lookup arg cs) + subp <- hoistList (cmdMatches cs) + case prefBacktrack prefs of + NoBacktrack -> lift $ do args <- get <* put [] fmap pure . lift $ enterContext arg subp *> runParserInfo subp args <* exitContext - (Just subp, Backtrack) -> fmap pure . lift . StateT $ \args -> + Backtrack -> fmap pure . lift . StateT $ \args -> enterContext arg subp *> runParser (infoPolicy subp) CmdStart (infoParser subp) args <* exitContext - (Just subp, SubparserInline) -> lift $ do + SubparserInline -> lift $ do lift $ enterContext arg subp return $ infoParser subp - (Nothing, _) -> mzero ArgReader rdr -> fmap pure . lift . lift $ runReadM (crReader rdr) arg _ -> mzero diff --git a/src/Options/Applicative/Extra.hs b/src/Options/Applicative/Extra.hs index 29f3255a..8b3f2721 100644 --- a/src/Options/Applicative/Extra.hs +++ b/src/Options/Applicative/Extra.hs @@ -7,6 +7,7 @@ module Options.Applicative.Extra ( helper, helperWith, hsubparser, + simpleVersioner, execParser, customExecParser, execParserPure, @@ -89,11 +90,24 @@ hsubparser :: Mod CommandFields a -> Parser a hsubparser m = mkParser d g rdr where Mod _ d g = metavar "COMMAND" `mappend` m - (groupName, cmds, subs) = mkCommand m - rdr = CmdReader groupName cmds (fmap add_helper . subs) + (groupName, cmds) = mkCommand m + rdr = CmdReader groupName ((fmap . fmap) add_helper cmds) add_helper pinfo = pinfo { infoParser = infoParser pinfo <**> helper } +-- | A hidden \"--version\" option that displays the version. +-- +-- > opts :: ParserInfo Sample +-- > opts = info (sample <**> simpleVersioner "v1.2.3") mempty +simpleVersioner :: String -- ^ Version string to be shown + -> Parser (a -> a) +simpleVersioner version = infoOption version $ + mconcat + [ long "version" + , help "Show version information" + , hidden + ] + -- | Run a program description. -- -- Parse command line arguments. Display help text and exit if any parse error @@ -154,7 +168,7 @@ execParserPure pprefs pinfo args = -- -- This function can be used, for example, to show the help text for a parser: -- --- @handleParseResult . Failure $ parserFailure pprefs pinfo ShowHelpText mempty@ +-- @handleParseResult . Failure $ parserFailure pprefs pinfo (ShowHelpText Nothing) mempty@ parserFailure :: ParserPrefs -> ParserInfo a -> ParseError -> [Context] -> ParserFailure ParserHelp @@ -313,10 +327,10 @@ parserFailure pprefs pinfo msg ctx0 = ParserFailure $ \progn -> MapReader _f r -> opt_completions reachability (opt { optMain = r }) FlagReader ns _ -> fmap showOption ns ArgReader _ -> [] - CmdReader _ ns _ | argumentIsUnreachable reachability + CmdReader _ ns | argumentIsUnreachable reachability -> [] | otherwise - -> ns + -> fst <$> ns _ -> mempty diff --git a/src/Options/Applicative/Help/Chunk.hs b/src/Options/Applicative/Help/Chunk.hs index 6fd39a91..881a3819 100644 --- a/src/Options/Applicative/Help/Chunk.hs +++ b/src/Options/Applicative/Help/Chunk.hs @@ -115,7 +115,7 @@ isEmpty = isNothing . unChunk -- > extractChunk . stringChunk = string stringChunk :: String -> Chunk Doc stringChunk "" = mempty -stringChunk s = pure (string s) +stringChunk s = pure (pretty s) -- | Convert a paragraph into a 'Chunk'. The resulting chunk is composed by the -- words of the original paragraph separated by softlines, so it will be diff --git a/src/Options/Applicative/Help/Core.hs b/src/Options/Applicative/Help/Core.hs index a27cbae9..76695b39 100644 --- a/src/Options/Applicative/Help/Core.hs +++ b/src/Options/Applicative/Help/Core.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} module Options.Applicative.Help.Core ( cmdDesc, briefDesc, @@ -23,9 +24,13 @@ import Control.Monad (guard) import Data.Function (on) import Data.List (sort, intersperse, groupBy) import Data.Foldable (any, foldl') -import Data.Maybe (maybeToList, catMaybes, fromMaybe) +import Data.Maybe (catMaybes, fromMaybe) +#if !MIN_VERSION_base(4,8,0) import Data.Monoid (mempty) +#endif +#if !MIN_VERSION_base(4,11,0) import Data.Semigroup (Semigroup (..)) +#endif import Prelude hiding (any) import Options.Applicative.Common @@ -54,7 +59,7 @@ optDesc pprefs style _reachability opt = meta2 = stringChunk $ optMetaVar2 opt descs = - map (string . showOption) names + map (pretty . showOption) names descriptions = listToChunk (intersperse (descSep style) descs) desc @@ -92,12 +97,11 @@ cmdDesc pprefs = mapParser desc where desc _ opt = case optMain opt of - CmdReader gn cmds p -> + CmdReader gn cmds -> (,) gn $ tabulate (prefTabulateFill pprefs) - [ (string cmd, align (extractChunk d)) - | cmd <- reverse cmds, - d <- maybeToList . fmap infoProgDesc $ p cmd + [ (pretty nm, align (extractChunk (infoProgDesc cmd))) + | (nm, cmd) <- reverse cmds ] _ -> mempty @@ -125,7 +129,7 @@ briefDesc' showOptional pprefs = | otherwise = filterOptional style = OptDescStyle - { descSep = string "|", + { descSep = pretty '|', descHidden = False, descGlobal = False } @@ -202,9 +206,9 @@ optionsDesc global pprefs = tabulate (prefTabulateFill pprefs) . catMaybes . map n = fst $ optDesc pprefs style info opt h = optHelp opt hdef = Chunk . fmap show_def . optShowDefault $ opt - show_def s = parens (string "default:" <+> string s) + show_def s = parens (pretty "default:" <+> pretty s) style = OptDescStyle - { descSep = string ",", + { descSep = pretty ',', descHidden = True, descGlobal = global } @@ -249,7 +253,7 @@ parserHelp pprefs p = group_title _ = mempty with_title :: String -> Chunk Doc -> Chunk Doc - with_title title = fmap (string title .$.) + with_title title = fmap (pretty title .$.) parserGlobals :: ParserPrefs -> Parser a -> ParserHelp @@ -263,11 +267,12 @@ parserGlobals pprefs p = -- | Generate option summary. parserUsage :: ParserPrefs -> Parser a -> String -> Doc parserUsage pprefs p progn = - hsep - [ string "Usage:", - string progn, - align (extractChunk (briefDesc pprefs p)) - ] + group $ + hsep + [ pretty "Usage:", + pretty progn, + hangAtIfOver 9 35 (extractChunk (briefDesc pprefs p)) + ] -- | Peek at the structure of the rendered tree within. -- diff --git a/src/Options/Applicative/Help/Pretty.hs b/src/Options/Applicative/Help/Pretty.hs index 9c8400b3..43d111a8 100644 --- a/src/Options/Applicative/Help/Pretty.hs +++ b/src/Options/Applicative/Help/Pretty.hs @@ -1,33 +1,63 @@ +{-# LANGUAGE CPP #-} module Options.Applicative.Help.Pretty - ( module Text.PrettyPrint.ANSI.Leijen + ( module Prettyprinter + , module Prettyprinter.Render.Terminal + , Doc + , SimpleDoc + , (.$.) + , () + , groupOrNestLine , altSep + , hangAtIfOver + + , prettyString ) where -import Control.Applicative -import Data.Semigroup ((<>)) +#if !MIN_VERSION_base(4,11,0) +import Data.Semigroup ((<>), mempty) +#endif +import qualified Data.Text.Lazy as Lazy -import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>), columns) -import Text.PrettyPrint.ANSI.Leijen.Internal (Doc (..), flatten) -import qualified Text.PrettyPrint.ANSI.Leijen as PP +import Prettyprinter hiding (Doc) +import qualified Prettyprinter as PP +import Prettyprinter.Render.Terminal import Prelude -(.$.) :: Doc -> Doc -> Doc -(.$.) = (PP.<$>) +type Doc = PP.Doc AnsiStyle +type SimpleDoc = SimpleDocStream AnsiStyle + +linebreak :: Doc +linebreak = flatAlt line mempty +(.$.) :: Doc -> Doc -> Doc +x .$. y = x <> line <> y +() :: Doc -> Doc -> Doc +x y = x <> softline <> y -- | Apply the function if we're not at the -- start of our nesting level. ifNotAtRoot :: (Doc -> Doc) -> Doc -> Doc -ifNotAtRoot f doc = - Nesting $ \i -> - Column $ \j -> - if i == j - then doc - else f doc +ifNotAtRoot = + ifElseAtRoot id +-- | Apply the function if we're not at the +-- start of our nesting level. +ifAtRoot :: (Doc -> Doc) -> Doc -> Doc +ifAtRoot = + flip ifElseAtRoot id + +-- | Apply the function if we're not at the +-- start of our nesting level. +ifElseAtRoot :: (Doc -> Doc) -> (Doc -> Doc) -> Doc -> Doc +ifElseAtRoot f g doc = + nesting $ \i -> + column $ \j -> + if i == j + then f doc + else g doc -- | Render flattened text on this line, or start -- a new line before rendering any text. @@ -36,9 +66,7 @@ ifNotAtRoot f doc = -- group. groupOrNestLine :: Doc -> Doc groupOrNestLine = - Union - <$> flatten - <*> ifNotAtRoot (line <>) . nest 2 + group . ifNotAtRoot (linebreak <>) . nest 2 -- | Separate items in an alternative with a pipe. @@ -53,4 +81,43 @@ groupOrNestLine = -- next line. altSep :: Doc -> Doc -> Doc altSep x y = - group (x <+> char '|' <> line) y + group (x <+> pretty '|' <> line) <> group linebreak <> y + + +-- | Printer hacks to get nice indentation for long commands +-- and subcommands. +-- +-- If we're starting this section over the desired width +--   (usually 1/3 of the ribbon), then we will make a line +-- break, indent all of the usage, and go. +-- +-- The ifAtRoot is an interesting clause. If this whole +-- operation is put under a `group` then the linebreak +-- will disappear; then item d will therefore not be at +-- the starting column, and it won't be indented more. +hangAtIfOver :: Int -> Int -> Doc -> Doc +hangAtIfOver i j d = + column $ \k -> + if k <= j then + align d + else + linebreak <> ifAtRoot (indent i) d + + +renderPretty :: Double -> Int -> Doc -> SimpleDocStream AnsiStyle +renderPretty ribbonFraction lineWidth + = layoutPretty LayoutOptions + { layoutPageWidth = AvailablePerLine lineWidth ribbonFraction } + +prettyString :: Double -> Int -> Doc -> String +prettyString ribbonFraction lineWidth + = streamToString + . renderPretty ribbonFraction lineWidth + +streamToString :: SimpleDocStream AnsiStyle -> String +streamToString sdoc = + let + rendered = + Prettyprinter.Render.Terminal.renderLazy sdoc + in + Lazy.unpack rendered diff --git a/src/Options/Applicative/Help/Types.hs b/src/Options/Applicative/Help/Types.hs index 0e2d05c0..e9743ca2 100644 --- a/src/Options/Applicative/Help/Types.hs +++ b/src/Options/Applicative/Help/Types.hs @@ -42,6 +42,5 @@ helpText (ParserHelp e s h u d b g f) = -- | Convert a help text to 'String'. renderHelp :: Int -> ParserHelp -> String renderHelp cols - = (`displayS` "") - . renderPretty 1.0 cols + = prettyString 1.0 cols . helpText diff --git a/src/Options/Applicative/Internal.hs b/src/Options/Applicative/Internal.hs index d5b854e7..b4831447 100644 --- a/src/Options/Applicative/Internal.hs +++ b/src/Options/Applicative/Internal.hs @@ -18,6 +18,7 @@ module Options.Applicative.Internal , ListT , takeListT , runListT + , hoistList , NondetT , cut @@ -172,9 +173,6 @@ bimapTStep :: (a -> b) -> (x -> y) -> TStep a x -> TStep b y bimapTStep _ _ TNil = TNil bimapTStep f g (TCons a x) = TCons (f a) (g x) -hoistList :: Monad m => [a] -> ListT m a -hoistList = foldr (\x xt -> ListT (return (TCons x xt))) mzero - takeListT :: Monad m => Int -> ListT m a -> ListT m a takeListT 0 = const mzero takeListT n = ListT . liftM (bimapTStep id (takeListT (n - 1))) . stepListT @@ -192,7 +190,7 @@ instance Monad m => Functor (ListT m) where . stepListT instance Monad m => Applicative (ListT m) where - pure = hoistList . pure + pure a = ListT (return (TCons a mzero)) (<*>) = ap instance Monad m => Monad (ListT m) where @@ -263,3 +261,8 @@ disamb allow_amb xs = do return $ case xs' of [x] -> Just x _ -> Nothing + +hoistList :: Alternative m => [a] -> m a +hoistList = foldr cons empty + where + cons x xs = pure x <|> xs diff --git a/src/Options/Applicative/Types.hs b/src/Options/Applicative/Types.hs index 7b741b3a..e83ab1d0 100644 --- a/src/Options/Applicative/Types.hs +++ b/src/Options/Applicative/Types.hs @@ -132,7 +132,7 @@ data ParserPrefs = ParserPrefs -- single space (default: False) , prefHelpShowGlobal :: Bool -- ^ when displaying subparsers' usage help, -- show parent options under a "global options" - -- section (default: True) + -- section (default: False) , prefTabulateFill ::Int -- ^ Indentation width for tables } deriving (Eq, Show) @@ -256,7 +256,7 @@ data OptReader a where -- | argument reader ArgReader :: CReader a -> OptReader a -- | command reader - CmdReader :: Maybe String -> [String] -> (String -> Maybe (ParserInfo a)) -> OptReader a + CmdReader :: (Maybe String) -> [(String, ParserInfo a)] -> OptReader a instance Functor OptReader where fmap f (OptReader ns cr e) = OptReader ns (fmap f cr) e @@ -264,7 +264,7 @@ instance Functor OptReader where fmap f (MapReader g r) = MapReader (f . g) r fmap f (FlagReader ns x) = FlagReader ns (f x) fmap f (ArgReader cr) = ArgReader (fmap f cr) - fmap f (CmdReader n cs g) = CmdReader n cs ((fmap . fmap) f . g) + fmap f (CmdReader n cs) = CmdReader n ((fmap . fmap . fmap) f cs) -- | A @Parser a@ is an option parser returning a value of type 'a'. data Parser a diff --git a/tests/Examples/Cabal.hs b/tests/Examples/Cabal.hs index 2d5560cc..7100a545 100644 --- a/tests/Examples/Cabal.hs +++ b/tests/Examples/Cabal.hs @@ -39,10 +39,6 @@ data BuildOpts = BuildOpts { buildDir :: FilePath } deriving Show -version :: Parser (a -> a) -version = infoOption "0.0.0" - ( long "version" - <> help "Print version information" ) parser :: Parser Args parser = runA $ proc () -> do @@ -60,7 +56,7 @@ parser = runA $ proc () -> do <> command "build" (info buildParser (progDesc "Make this package ready for installation")) ) -< () - A version >>> A helper -< Args opts cmds + A (simpleVersioner "0.0.0") >>> A helper -< Args opts cmds commonOpts :: Parser CommonOpts commonOpts = CommonOpts diff --git a/tests/Examples/LongSub.hs b/tests/Examples/LongSub.hs new file mode 100644 index 00000000..22ced292 --- /dev/null +++ b/tests/Examples/LongSub.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE CPP #-} +module Examples.LongSub where + +import Data.Monoid +import Options.Applicative + +#if __GLASGOW_HASKELL__ <= 702 +(<>) :: Monoid a => a -> a -> a +(<>) = mappend +#endif + +data Sample + = Hello [String] + | Goodbye + deriving (Eq, Show) + +hello :: Parser Sample +hello = + Hello + <$> many (argument str (metavar "TARGET...")) + <* switch (long "first-flag") + <* switch (long "second-flag") + <* switch (long "third-flag") + <* switch (long "fourth-flag") + +sample :: Parser Sample +sample = hsubparser + ( command "hello-very-long-sub" + (info hello + (progDesc "Print greeting")) + ) + +opts :: ParserInfo Sample +opts = info (sample <**> helper) idm diff --git a/tests/cabal.err.txt b/tests/cabal.err.txt index b419bb5f..c0f843be 100644 --- a/tests/cabal.err.txt +++ b/tests/cabal.err.txt @@ -9,4 +9,4 @@ Available options: Global options: -v,--verbose LEVEL Set verbosity to LEVEL - --version Print version information + --version Show version information diff --git a/tests/formatting-long-subcommand.err.txt b/tests/formatting-long-subcommand.err.txt new file mode 100644 index 00000000..a6179fe3 --- /dev/null +++ b/tests/formatting-long-subcommand.err.txt @@ -0,0 +1,9 @@ +Usage: formatting-long-subcommand hello-very-long-sub + [TARGET...] [--first-flag] + [--second-flag] [--third-flag] + [--fourth-flag] + + Print greeting + +Available options: + -h,--help Show this help text diff --git a/tests/test.hs b/tests/test.hs index affda078..c2c4dc72 100644 --- a/tests/test.hs +++ b/tests/test.hs @@ -9,6 +9,7 @@ import qualified Examples.Commands as Commands import qualified Examples.Cabal as Cabal import qualified Examples.Alternatives as Alternatives import qualified Examples.Formatting as Formatting +import qualified Examples.LongSub as LongSub import Control.Applicative import Control.Monad @@ -27,7 +28,7 @@ import qualified Options.Applicative.NonEmpty import qualified Options.Applicative.Help as H -import Options.Applicative.Help.Pretty (Doc, SimpleDoc(..)) +import Options.Applicative.Help.Pretty (Doc) import qualified Options.Applicative.Help.Pretty as Doc import Options.Applicative.Help.Chunk import Options.Applicative.Help.Levenshtein @@ -317,6 +318,48 @@ prop_ambiguous = once $ result = execParserPure (prefs disambiguate) i ["--ba"] in assertError result (\_ -> property succeeded) +prop_disambiguate_in_same_subparsers :: Property +prop_disambiguate_in_same_subparsers = once $ + let p0 = subparser (command "oranges" (info (pure "oranges") idm) <> command "apples" (info (pure "apples") idm) <> metavar "B") + i = info (p0 <**> helper) idm + result = execParserPure (prefs disambiguate) i ["orang"] + in assertResult result ((===) "oranges") + +prop_disambiguate_commands_in_separate_subparsers :: Property +prop_disambiguate_commands_in_separate_subparsers = once $ + let p2 = subparser (command "oranges" (info (pure "oranges") idm) <> metavar "B") + p1 = subparser (command "apples" (info (pure "apples") idm) <> metavar "C") + p0 = p1 <|> p2 + i = info (p0 <**> helper) idm + result = execParserPure (prefs disambiguate) i ["orang"] + in assertResult result ((===) "oranges") + +prop_fail_ambiguous_commands_in_same_subparser :: Property +prop_fail_ambiguous_commands_in_same_subparser = once $ + let p0 = subparser (command "oranges" (info (pure ()) idm) <> command "orangutans" (info (pure ()) idm) <> metavar "B") + i = info (p0 <**> helper) idm + result = execParserPure (prefs disambiguate) i ["orang"] + in assertError result (\_ -> property succeeded) + +prop_fail_ambiguous_commands_in_separate_subparser :: Property +prop_fail_ambiguous_commands_in_separate_subparser = once $ + let p2 = subparser (command "oranges" (info (pure ()) idm) <> metavar "B") + p1 = subparser (command "orangutans" (info (pure ()) idm) <> metavar "C") + p0 = p1 <|> p2 + i = info (p0 <**> helper) idm + result = execParserPure (prefs disambiguate) i ["orang"] + in assertError result (\_ -> property succeeded) + +prop_without_disambiguation_same_named_commands_should_parse_in_order :: Property +prop_without_disambiguation_same_named_commands_should_parse_in_order = once $ + let p3 = subparser (command "b" (info (pure ()) idm) <> metavar "B") + p2 = subparser (command "a" (info (pure ()) idm) <> metavar "B") + p1 = subparser (command "a" (info (pure ()) idm) <> metavar "C") + p0 = (,,) <$> p1 <*> p2 <*> p3 + i = info (p0 <**> helper) idm + result = execParserPure defaultPrefs i ["b", "a", "a"] + in assertResult result ((===) ((), (), ())) + prop_completion :: Property prop_completion = once . ioProperty $ let p = (,) @@ -1025,15 +1068,25 @@ prop_completion_biOption_second_value = once . ioProperty $ Failure _ -> return $ counterexample "unexpected failure" failed Success val -> return $ counterexample ("unexpected result " ++ show val) failed + +prop_long_command_line_flow :: Property +prop_long_command_line_flow = once $ + let p = LongSub.sample <**> helper + i = info p + ( progDesc (concat + [ "This is a very long program description. " + , "This text should be automatically wrapped " + , "to fit the size of the terminal" ]) ) + in checkHelpTextWith ExitSuccess (prefs (columns 50)) "formatting-long-subcommand" i ["hello-very-long-sub", "--help"] + --- deriving instance Arbitrary a => Arbitrary (Chunk a) -deriving instance Eq SimpleDoc -deriving instance Show SimpleDoc -equalDocs :: Float -> Int -> Doc -> Doc -> Property -equalDocs f w d1 d2 = Doc.renderPretty f w d1 - === Doc.renderPretty f w d2 + +equalDocs :: Double -> Int -> Doc -> Doc -> Property +equalDocs f w d1 d2 = Doc.prettyString f w d1 + === Doc.prettyString f w d2 prop_listToChunk_1 :: [String] -> Property prop_listToChunk_1 xs = isEmpty (listToChunk xs) === null xs @@ -1047,10 +1100,10 @@ prop_extractChunk_1 x = extractChunk (pure x) === x prop_extractChunk_2 :: Chunk String -> Property prop_extractChunk_2 x = extractChunk (fmap pure x) === x -prop_stringChunk_1 :: Positive Float -> Positive Int -> String -> Property +prop_stringChunk_1 :: Positive Double -> Positive Int -> String -> Property prop_stringChunk_1 (Positive f) (Positive w) s = equalDocs f w (extractChunk (stringChunk s)) - (Doc.string s) + (Doc.pretty s) prop_stringChunk_2 :: String -> Property prop_stringChunk_2 s = isEmpty (stringChunk s) === null s