diff --git a/.github/actions/package/action.yml b/.github/actions/package/action.yml new file mode 100644 index 0000000..bc0a2cd --- /dev/null +++ b/.github/actions/package/action.yml @@ -0,0 +1,62 @@ +name: Upload built package +description: >- + Build and update package. + +inputs: + upload-files: + required: false + default: 'pkg/*.gem' + description: File name pattern to upload. + + package-name: + required: false + default: '${GITHUB_REPOSITORY#*/}-${RUNNER_OS%-*}' + description: Package name to upload. + + build-program: + required: false + default: rake build + description: Command to build package files. + +runs: + using: composite + + steps: + - id: setup + run: | + : Setup + PS4="##[command]"; set -x + : Fetch deeper for changelogs + git fetch --force --no-tags origin 'refs/tags/v*:refs/tags/v*' + set -- "$(git symbolic-ref --short HEAD)" $(git tag --list --no-contains HEAD --sort -version:refname) + branch=$1 prev=$2 + git fetch ${prev:+--shallow-exclude=}${prev:---unshallow} origin ${branch} + : Re-checkout with LF + git config core.autocrlf false + git config core.eol lf + git checkout -f + shell: bash + + - id: build + run: | + : Build + if command -v shasum > /dev/null; then + shasum=(shasum -a 256 -b) # Ubuntu, macOS + elif command -v sha256sum > /dev/null; then + shasum=(sha256sum -b) # Windows + else # fallback + shasum=(ruby -rdigest -e "ARGV.each{|f| print Digest::SHA256.file(f).hexdigest, ' *'; puts f}") + fi + PS4="##[command]"; set -x + ${{ inputs.build-program }} + : Show info + ls -l ${{ inputs.upload-files }} + "${shasum[@]}" ${{ inputs.upload-files }} + echo pkg="${{ inputs.package-name }}" >> $GITHUB_OUTPUT + shell: bash + + - id: upload + uses: actions/upload-artifact@v4 + with: + path: ${{ inputs.upload-files }} + name: ${{ steps.build.outputs.pkg }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..065e78a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,84 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '35 1 * * 0' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/push_gem.yml b/.github/workflows/push_gem.yml new file mode 100644 index 0000000..ceaa5d0 --- /dev/null +++ b/.github/workflows/push_gem.yml @@ -0,0 +1,46 @@ +name: Publish gem to rubygems.org + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + push: + if: github.repository == 'ruby/optparse' + runs-on: ubuntu-latest + + environment: + name: rubygems.org + url: https://rubygems.org/gems/optparse + + permissions: + contents: write + id-token: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.1.4 + + - name: Set up Ruby + uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 + with: + bundler-cache: true + ruby-version: ruby + + - name: Publish to RubyGems + uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 + + - name: Create GitHub release + run: | + tag_name="$(git describe --tags --abbrev=0)" + gh release create "${tag_name}" --verify-tag --generate-notes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98a7642..250d3f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,32 +1,34 @@ name: test -on: [push, pull_request] +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '3 11 * * 4' jobs: ruby-versions: - runs-on: ubuntu-latest - outputs: - versions: ${{ steps.versions.outputs.value }} - steps: - - id: versions - run: | - versions=$(curl -s 'https://cache.ruby-lang.org/pub/misc/ci_versions/all.json' | jq -c '. + ["2.5"]') - echo "::set-output name=value::${versions}" + uses: ruby/actions/.github/workflows/ruby_versions.yml@master + with: + min_version: 2.5 + test: needs: ruby-versions name: build (${{ matrix.ruby }} / ${{ matrix.os }}) strategy: matrix: ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} - os: [ ubuntu-latest, macos-latest ] + os: [ ubuntu-latest, macos-latest, windows-latest ] + exclude: + - { os: macos-latest, ruby: 2.5 } + - { os: windows-latest, ruby: truffleruby-head } + - { os: windows-latest, ruby: truffleruby } + - { os: windows-latest, ruby: jruby-head } + - { os: windows-latest, ruby: jruby } runs-on: ${{ matrix.os }} steps: - - name: git config - run: | - git config --global core.autocrlf false - git config --global core.eol lf - git config --global advice.detachedHead 0 - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: @@ -36,17 +38,5 @@ jobs: - name: Run test run: rake test - - id: build - run: | - rake build - ls -l pkg/*.gem - shasum -a 256 pkg/*.gem - echo "::set-output name=pkg::${GITHUB_REPOSITORY#*/}-${RUNNING_OS%-*}" - env: - RUNNING_OS: ${{matrix.os}} - shell: bash - - name: Upload package - uses: actions/upload-artifact@v3 - with: - path: pkg/*.gem - name: ${{steps.build.outputs.pkg}} + - uses: ./.github/actions/package + if: ${{ matrix.ruby == needs.ruby-versions.outputs.latest }} diff --git a/.rdoc_options b/.rdoc_options new file mode 100644 index 0000000..24fe2d7 --- /dev/null +++ b/.rdoc_options @@ -0,0 +1,5 @@ +--- +page_dir: doc +main_page: README.md +title: Documentation for OptionParser +op_dir: rdoc diff --git a/BSDL b/BSDL new file mode 100644 index 0000000..66d9359 --- /dev/null +++ b/BSDL @@ -0,0 +1,22 @@ +Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/Gemfile b/Gemfile index eb86192..86af132 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,4 @@ source "https://rubygems.org" gem "rake" gem "test-unit" +gem "test-unit-ruby-core" diff --git a/README.md b/README.md index 67d829f..160a4e7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ p ARGV ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). diff --git a/Rakefile b/Rakefile index 29d9a18..8482e91 100644 --- a/Rakefile +++ b/Rakefile @@ -7,12 +7,6 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList["test/**/test_*.rb"] end -task :sync_tool, [:from] do |t, from: "../ruby"| - cp "#{from}/tool/lib/core_assertions.rb", "./test/lib" - cp "#{from}/tool/lib/envutil.rb", "./test/lib" - cp "#{from}/tool/lib/find_executable.rb", "./test/lib" -end - task :default => :test task :rdoc do diff --git a/doc/optparse/argument_converters.rdoc b/doc/optparse/argument_converters.rdoc index ac659da..5327298 100644 --- a/doc/optparse/argument_converters.rdoc +++ b/doc/optparse/argument_converters.rdoc @@ -1,7 +1,7 @@ == Argument Converters An option can specify that its argument is to be converted -from the default \String to an instance of another class. +from the default +String+ to an instance of another class. === Contents @@ -27,13 +27,13 @@ from the default \String to an instance of another class. === Built-In Argument Converters -\OptionParser has a number of built-in argument converters, ++OptionParser+ has a number of built-in argument converters, which are demonstrated below. -==== \Date +==== +Date+ File +date.rb+ -defines an option whose argument is to be converted to a \Date object. +defines an option whose argument is to be converted to a +Date+ object. The argument is converted by method Date#parse. :include: ruby/date.rb @@ -47,10 +47,10 @@ Executions: $ ruby date.rb --date "3rd Feb 2001" [#, Date] -==== \DateTime +==== +DateTime+ File +datetime.rb+ -defines an option whose argument is to be converted to a \DateTime object. +defines an option whose argument is to be converted to a +DateTime+ object. The argument is converted by method DateTime#parse. :include: ruby/datetime.rb @@ -64,10 +64,10 @@ Executions: $ ruby datetime.rb --datetime "3rd Feb 2001 04:05:06 PM" [#, DateTime] -==== \Time +==== +Time+ File +time.rb+ -defines an option whose argument is to be converted to a \Time object. +defines an option whose argument is to be converted to a +Time+ object. The argument is converted by method Time#httpdate or Time#parse. :include: ruby/time.rb @@ -79,10 +79,10 @@ Executions: $ ruby time.rb --time 2010-10-31 [2010-10-31 00:00:00 -0500, Time] -==== \URI +==== +URI+ File +uri.rb+ -defines an option whose argument is to be converted to a \URI object. +defines an option whose argument is to be converted to a +URI+ object. The argument is converted by method URI#parse. :include: ruby/uri.rb @@ -96,10 +96,10 @@ Executions: $ ruby uri.rb --uri file://~/var [#, URI::File] -==== \Shellwords +==== +Shellwords+ File +shellwords.rb+ -defines an option whose argument is to be converted to an \Array object by method +defines an option whose argument is to be converted to an +Array+ object by method Shellwords#shellwords. :include: ruby/shellwords.rb @@ -111,10 +111,10 @@ Executions: $ ruby shellwords.rb --shellwords "here are 'two words'" [["here", "are", "two words"], Array] -==== \Integer +==== +Integer+ File +integer.rb+ -defines an option whose argument is to be converted to an \Integer object. +defines an option whose argument is to be converted to an +Integer+ object. The argument is converted by method Kernel#Integer. :include: ruby/integer.rb @@ -132,10 +132,10 @@ Executions: $ ruby integer.rb --integer 0b100 [4, Integer] -==== \Float +==== +Float+ File +float.rb+ -defines an option whose argument is to be converted to a \Float object. +defines an option whose argument is to be converted to a +Float+ object. The argument is converted by method Kernel#Float. :include: ruby/float.rb @@ -151,11 +151,11 @@ Executions: $ ruby float.rb --float 1.234E-2 [0.01234, Float] -==== \Numeric +==== +Numeric+ File +numeric.rb+ defines an option whose argument is to be converted to an instance -of \Rational, \Float, or \Integer. +of +Rational+, +Float+, or +Integer+. The argument is converted by method Kernel#Rational, Kernel#Float, or Kernel#Integer. @@ -170,10 +170,10 @@ Executions: $ ruby numeric.rb --numeric 3 [3, Integer] -==== \DecimalInteger +==== +DecimalInteger+ File +decimal_integer.rb+ -defines an option whose argument is to be converted to an \Integer object. +defines an option whose argument is to be converted to an +Integer+ object. The argument is converted by method Kernel#Integer. :include: ruby/decimal_integer.rb @@ -192,10 +192,10 @@ Executions: $ ruby decimal_integer.rb --decimal_integer -0100 [-100, Integer] -==== \OctalInteger +==== +OctalInteger+ File +octal_integer.rb+ -defines an option whose argument is to be converted to an \Integer object. +defines an option whose argument is to be converted to an +Integer+ object. The argument is converted by method Kernel#Integer. :include: ruby/octal_integer.rb @@ -212,10 +212,10 @@ Executions: $ ruby octal_integer.rb --octal_integer 0100 [64, Integer] -==== \DecimalNumeric +==== +DecimalNumeric+ File +decimal_numeric.rb+ -defines an option whose argument is to be converted to an \Integer object. +defines an option whose argument is to be converted to an +Integer+ object. The argument is converted by method Kernel#Integer :include: ruby/decimal_numeric.rb @@ -232,7 +232,7 @@ Executions: $ ruby decimal_numeric.rb --decimal_numeric 0100 [64, Integer] -==== \TrueClass +==== +TrueClass+ File +true_class.rb+ defines an option whose argument is to be converted to +true+ or +false+. @@ -259,7 +259,7 @@ Executions: $ ruby true_class.rb --true_class nil [false, FalseClass] -==== \FalseClass +==== +FalseClass+ File +false_class.rb+ defines an option whose argument is to be converted to +true+ or +false+. @@ -286,10 +286,10 @@ Executions: $ ruby false_class.rb --false_class + [true, TrueClass] -==== \Object +==== +Object+ File +object.rb+ -defines an option whose argument is not to be converted from \String. +defines an option whose argument is not to be converted from +String+. :include: ruby/object.rb @@ -300,10 +300,10 @@ Executions: $ ruby object.rb --object nil ["nil", String] -==== \String +==== +String+ File +string.rb+ -defines an option whose argument is not to be converted from \String. +defines an option whose argument is not to be converted from +String+. :include: ruby/string.rb @@ -314,10 +314,10 @@ Executions: $ ruby string.rb --string nil ["nil", String] -==== \Array +==== +Array+ File +array.rb+ -defines an option whose argument is to be converted from \String +defines an option whose argument is to be converted from +String+ to an array of strings, based on comma-separated substrings. :include: ruby/array.rb @@ -331,10 +331,10 @@ Executions: $ ruby array.rb --array "foo, bar, baz" [["foo", " bar", " baz"], Array] -==== \Regexp +==== +Regexp+ File +regexp.rb+ -defines an option whose argument is to be converted to a \Regexp object. +defines an option whose argument is to be converted to a +Regexp+ object. :include: ruby/regexp.rb @@ -352,7 +352,7 @@ To create a custom converter, call OptionParser#accept with: - A block that accepts the argument and returns the converted value. This custom converter accepts any argument and converts it, -if possible, to a \Complex object. +if possible, to a +Complex+ object. :include: ruby/custom_converter.rb @@ -377,4 +377,4 @@ Executions: $ ruby match_converter.rb --capitalize foo ["Foo", String] $ ruby match_converter.rb --capitalize "foo bar" - match_converter.rb:9:in `
': invalid argument: --capitalize foo bar (OptionParser::InvalidArgument) + match_converter.rb:9:in '
': invalid argument: --capitalize foo bar (OptionParser::InvalidArgument) diff --git a/doc/optparse/option_params.rdoc b/doc/optparse/option_params.rdoc index ace2c42..575ee66 100644 --- a/doc/optparse/option_params.rdoc +++ b/doc/optparse/option_params.rdoc @@ -1,6 +1,6 @@ == Parameters for New Options -Option-creating methods in \OptionParser +Option-creating methods in +OptionParser+ accept arguments that determine the behavior of a new option: - OptionParser#on @@ -31,7 +31,7 @@ Contents: - {Long Names with Optional Arguments}[#label-Long+Names+with+Optional+Arguments] - {Long Names with Negation}[#label-Long+Names+with+Negation] - {Mixed Names}[#label-Mixed+Names] -- {Argument Styles}[#label-Argument+Styles] +- {Argument Strings}[#label-Argument+Strings] - {Argument Values}[#label-Argument+Values] - {Explicit Argument Values}[#label-Explicit+Argument+Values] - {Explicit Values in Array}[#label-Explicit+Values+in+Array] @@ -91,7 +91,7 @@ Executions: Usage: short_required [options] -xXXX Short name with required argument $ ruby short_required.rb -x - short_required.rb:6:in `
': missing argument: -x (OptionParser::MissingArgument) + short_required.rb:6:in '
': missing argument: -x (OptionParser::MissingArgument) $ ruby short_required.rb -x FOO ["-x", "FOO"] @@ -181,7 +181,7 @@ Executions: Usage: long_required [options] --xxx XXX Long name with required argument $ ruby long_required.rb --xxx - long_required.rb:6:in `
': missing argument: --xxx (OptionParser::MissingArgument) + long_required.rb:6:in '
': missing argument: --xxx (OptionParser::MissingArgument) $ ruby long_required.rb --xxx FOO ["--xxx", "FOO"] @@ -243,11 +243,11 @@ Usage: mixed_names [options] $ ruby mixed_names.rb --xxx ["--xxx", true] $ ruby mixed_names.rb -y - mixed_names.rb:12:in `
': missing argument: -y (OptionParser::MissingArgument) + mixed_names.rb:12:in '
': missing argument: -y (OptionParser::MissingArgument) $ ruby mixed_names.rb -y FOO ["--yyy", "FOO"] $ ruby mixed_names.rb --yyy - mixed_names.rb:12:in `
': missing argument: --yyy (OptionParser::MissingArgument) + mixed_names.rb:12:in '
': missing argument: --yyy (OptionParser::MissingArgument) $ ruby mixed_names.rb --yyy BAR ["--yyy", "BAR"] $ ruby mixed_names.rb -z @@ -279,7 +279,7 @@ Executions: Usage: argument_keywords [options] -x, --xxx Required argument $ ruby argument_styles.rb --xxx - argument_styles.rb:6:in `
': missing argument: --xxx (OptionParser::MissingArgument) + argument_styles.rb:6:in '
': missing argument: --xxx (OptionParser::MissingArgument) $ ruby argument_styles.rb --xxx FOO ["--xxx", "FOO"] @@ -298,7 +298,7 @@ Executions: Usage: argument_strings [options] -x, --xxx=XXX Required argument $ ruby argument_strings.rb --xxx - argument_strings.rb:9:in `
': missing argument: --xxx (OptionParser::MissingArgument) + argument_strings.rb:9:in '
': missing argument: --xxx (OptionParser::MissingArgument) $ ruby argument_strings.rb --xxx FOO ["--xxx", "FOO"] @@ -331,7 +331,7 @@ Executions: -xXXX Values for required argument -y [YYY] Values for optional argument $ ruby explicit_array_values.rb -x - explicit_array_values.rb:9:in `
': missing argument: -x (OptionParser::MissingArgument) + explicit_array_values.rb:9:in '
': missing argument: -x (OptionParser::MissingArgument) $ ruby explicit_array_values.rb -x foo ["-x", "foo"] $ ruby explicit_array_values.rb -x f @@ -339,9 +339,9 @@ Executions: $ ruby explicit_array_values.rb -x bar ["-x", "bar"] $ ruby explicit_array_values.rb -y ba - explicit_array_values.rb:9:in `
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) + explicit_array_values.rb:9:in '
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) $ ruby explicit_array_values.rb -x baz - explicit_array_values.rb:9:in `
': invalid argument: -x baz (OptionParser::InvalidArgument) + explicit_array_values.rb:9:in '
': invalid argument: -x baz (OptionParser::InvalidArgument) ===== Explicit Values in Hash @@ -361,7 +361,7 @@ Executions: -xXXX Values for required argument -y [YYY] Values for optional argument $ ruby explicit_hash_values.rb -x - explicit_hash_values.rb:9:in `
': missing argument: -x (OptionParser::MissingArgument) + explicit_hash_values.rb:9:in '
': missing argument: -x (OptionParser::MissingArgument) $ ruby explicit_hash_values.rb -x foo ["-x", 0] $ ruby explicit_hash_values.rb -x f @@ -369,7 +369,7 @@ Executions: $ ruby explicit_hash_values.rb -x bar ["-x", 1] $ ruby explicit_hash_values.rb -x baz - explicit_hash_values.rb:9:in `
': invalid argument: -x baz (OptionParser::InvalidArgument) + explicit_hash_values.rb:9:in '
': invalid argument: -x baz (OptionParser::InvalidArgument) $ ruby explicit_hash_values.rb -y ["-y", nil] $ ruby explicit_hash_values.rb -y baz @@ -377,14 +377,15 @@ Executions: $ ruby explicit_hash_values.rb -y bat ["-y", 3] $ ruby explicit_hash_values.rb -y ba - explicit_hash_values.rb:9:in `
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) + explicit_hash_values.rb:9:in '
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) $ ruby explicit_hash_values.rb -y bam ["-y", nil] ==== Argument Value Patterns You can restrict permissible argument values -by specifying a Regexp that the given argument must match. +by specifying a +Regexp+ that the given argument must match, +or a +Range+ or +Array+ that the converted value must be included in. File +matched_values.rb+ defines options with matched argument values. @@ -395,17 +396,27 @@ Executions: $ ruby matched_values.rb --help Usage: matched_values [options] --xxx XXX Matched values + --yyy YYY Check by range + --zzz ZZZ Check by list $ ruby matched_values.rb --xxx foo ["--xxx", "foo"] $ ruby matched_values.rb --xxx FOO ["--xxx", "FOO"] $ ruby matched_values.rb --xxx bar - matched_values.rb:6:in `
': invalid argument: --xxx bar (OptionParser::InvalidArgument) + matched_values.rb:12:in '
': invalid argument: --xxx bar (OptionParser::InvalidArgument) + $ ruby matched_values.rb --yyy 1 + ["--yyy", 1] + $ ruby matched_values.rb --yyy 4 + matched_values.rb:12:in '
': invalid argument: --yyy 4 (OptionParser::InvalidArgument) + $ ruby matched_values.rb --zzz 1 + ["--zzz", 1] + $ ruby matched_values.rb --zzz 2 + matched_values.rb:12:in '
': invalid argument: --zzz 2 (OptionParser::InvalidArgument) === Argument Converters An option can specify that its argument is to be converted -from the default \String to an instance of another class. +from the default +String+ to an instance of another class. There are a number of built-in converters. You can also define custom converters. diff --git a/doc/optparse/ruby/argument_abbreviation.rb b/doc/optparse/ruby/argument_abbreviation.rb new file mode 100644 index 0000000..49007eb --- /dev/null +++ b/doc/optparse/ruby/argument_abbreviation.rb @@ -0,0 +1,9 @@ +require 'optparse' +parser = OptionParser.new +parser.on('-x', '--xxx=VALUE', %w[ABC def], 'Argument abbreviations') do |value| + p ['--xxx', value] +end +parser.on('-y', '--yyy=VALUE', {"abc"=>"XYZ", def: "FOO"}, 'Argument abbreviations') do |value| + p ['--yyy', value] +end +parser.parse! diff --git a/doc/optparse/ruby/matched_values.rb b/doc/optparse/ruby/matched_values.rb index f184ca8..a1aba14 100644 --- a/doc/optparse/ruby/matched_values.rb +++ b/doc/optparse/ruby/matched_values.rb @@ -3,4 +3,10 @@ parser.on('--xxx XXX', /foo/i, 'Matched values') do |value| p ['--xxx', value] end +parser.on('--yyy YYY', Integer, 'Check by range', 1..3) do |value| + p ['--yyy', value] +end +parser.on('--zzz ZZZ', Integer, 'Check by list', [1, 3, 4]) do |value| + p ['--zzz', value] +end parser.parse! diff --git a/doc/optparse/tutorial.rdoc b/doc/optparse/tutorial.rdoc index b950898..1134f94 100644 --- a/doc/optparse/tutorial.rdoc +++ b/doc/optparse/tutorial.rdoc @@ -1,10 +1,10 @@ == Tutorial -=== Why \OptionParser? +=== Why +OptionParser+? When a Ruby program executes, it captures its command-line arguments and options into variable ARGV. -This simple program just prints its \ARGV: +This simple program just prints its +ARGV+: :include: ruby/argv.rb @@ -18,7 +18,7 @@ the command-line options. OptionParser offers methods for parsing and handling those options. -With \OptionParser, you can define options so that for each option: +With +OptionParser+, you can define options so that for each option: - The code that defines the option and code that handles that option are in the same place. @@ -55,7 +55,7 @@ The class also has method #help, which displays automatically-generated help tex - {Argument Converters}[#label-Argument+Converters] - {Help}[#label-Help] - {Top List and Base List}[#label-Top+List+and+Base+List] -- {Defining Options}[#label-Defining+Options] +- {Methods for Defining Options}[#label-Methods+for+Defining+Options] - {Parsing}[#label-Parsing] - {Method parse!}[#label-Method+parse-21] - {Method parse}[#label-Method+parse] @@ -66,10 +66,10 @@ The class also has method #help, which displays automatically-generated help tex === To Begin With -To use \OptionParser: +To use +OptionParser+: -1. Require the \OptionParser code. -2. Create an \OptionParser object. +1. Require the +OptionParser+ code. +2. Create an +OptionParser+ object. 3. Define one or more options. 4. Parse the command line. @@ -92,9 +92,9 @@ the block defined for the option is called with the argument value. An invalid option raises an exception. Method #parse!, which is used most often in this tutorial, -removes from \ARGV the options and arguments it finds, +removes from +ARGV+ the options and arguments it finds, leaving other non-option arguments for the program to handle on its own. -The method returns the possibly-reduced \ARGV array. +The method returns the possibly-reduced +ARGV+ array. Executions: @@ -111,11 +111,11 @@ Executions: ["x", true] ["input_file.txt", "output_file.txt"] $ ruby basic.rb -a - basic.rb:16:in `
': invalid option: -a (OptionParser::InvalidOption) + basic.rb:16:in '
': invalid option: -a (OptionParser::InvalidOption) === Defining Options -A common way to define an option in \OptionParser +A common way to define an option in +OptionParser+ is with instance method OptionParser#on. The method may be called with any number of arguments @@ -232,11 +232,11 @@ Executions: $ ruby mixed_names.rb --xxx ["--xxx", true] $ ruby mixed_names.rb -y - mixed_names.rb:12:in `
': missing argument: -y (OptionParser::MissingArgument) + mixed_names.rb:12:in '
': missing argument: -y (OptionParser::MissingArgument) $ ruby mixed_names.rb -y FOO ["--yyy", "FOO"] $ ruby mixed_names.rb --yyy - mixed_names.rb:12:in `
': missing argument: --yyy (OptionParser::MissingArgument) + mixed_names.rb:12:in '
': missing argument: --yyy (OptionParser::MissingArgument) $ ruby mixed_names.rb --yyy BAR ["--yyy", "BAR"] $ ruby mixed_names.rb -z @@ -270,9 +270,9 @@ Executions: $ ruby name_abbrev.rb --draft ["--draft", true] $ ruby name_abbrev.rb --d - name_abbrev.rb:9:in `
': ambiguous option: --d (OptionParser::AmbiguousOption) + name_abbrev.rb:9:in '
': ambiguous option: --d (OptionParser::AmbiguousOption) $ ruby name_abbrev.rb --dr - name_abbrev.rb:9:in `
': ambiguous option: --dr (OptionParser::AmbiguousOption) + name_abbrev.rb:9:in '
': ambiguous option: --dr (OptionParser::AmbiguousOption) $ ruby name_abbrev.rb --dry ["--dry-run", true] $ ruby name_abbrev.rb --dra @@ -285,7 +285,7 @@ You can disable abbreviation using method +require_exact+. Executions: $ ruby no_abbreviation.rb --dry-ru - no_abbreviation.rb:10:in `
': invalid option: --dry-ru (OptionParser::InvalidOption) + no_abbreviation.rb:10:in '
': invalid option: --dry-ru (OptionParser::InvalidOption) $ ruby no_abbreviation.rb --dry-run ["--dry-run", true] @@ -323,7 +323,7 @@ Executions: Omitting a required argument raises an error: $ ruby required_argument.rb -x - required_argument.rb:9:in `
': missing argument: -x (OptionParser::MissingArgument) + required_argument.rb:9:in '
': missing argument: -x (OptionParser::MissingArgument) ==== Option with Optional Argument @@ -351,6 +351,29 @@ Executions: Omitting an optional argument does not raise an error. +==== Argument Abbreviations + +Specify an argument list as an Array or a Hash. + + :include: ruby/argument_abbreviation.rb + +When an argument is abbreviated, the expanded argument yielded. + +Executions: + + $ ruby argument_abbreviation.rb --help + Usage: argument_abbreviation [options] + Usage: argument_abbreviation [options] + -x, --xxx=VALUE Argument abbreviations + -y, --yyy=VALUE Argument abbreviations + $ ruby argument_abbreviation.rb --xxx A + ["--xxx", "ABC"] + $ ruby argument_abbreviation.rb --xxx c + argument_abbreviation.rb:9:in '
': invalid argument: --xxx c (OptionParser::InvalidArgument) + $ ruby argument_abbreviation.rb --yyy a --yyy d + ["--yyy", "XYZ"] + ["--yyy", "FOO"] + === Argument Values Permissible argument values may be restricted @@ -380,7 +403,7 @@ Executions: -xXXX Values for required argument -y [YYY] Values for optional argument $ ruby explicit_array_values.rb -x - explicit_array_values.rb:9:in `
': missing argument: -x (OptionParser::MissingArgument) + explicit_array_values.rb:9:in '
': missing argument: -x (OptionParser::MissingArgument) $ ruby explicit_array_values.rb -x foo ["-x", "foo"] $ ruby explicit_array_values.rb -x f @@ -388,9 +411,9 @@ Executions: $ ruby explicit_array_values.rb -x bar ["-x", "bar"] $ ruby explicit_array_values.rb -y ba - explicit_array_values.rb:9:in `
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) + explicit_array_values.rb:9:in '
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) $ ruby explicit_array_values.rb -x baz - explicit_array_values.rb:9:in `
': invalid argument: -x baz (OptionParser::InvalidArgument) + explicit_array_values.rb:9:in '
': invalid argument: -x baz (OptionParser::InvalidArgument) ===== Explicit Values in Hash @@ -410,7 +433,7 @@ Executions: -xXXX Values for required argument -y [YYY] Values for optional argument $ ruby explicit_hash_values.rb -x - explicit_hash_values.rb:9:in `
': missing argument: -x (OptionParser::MissingArgument) + explicit_hash_values.rb:9:in '
': missing argument: -x (OptionParser::MissingArgument) $ ruby explicit_hash_values.rb -x foo ["-x", 0] $ ruby explicit_hash_values.rb -x f @@ -418,7 +441,7 @@ Executions: $ ruby explicit_hash_values.rb -x bar ["-x", 1] $ ruby explicit_hash_values.rb -x baz - explicit_hash_values.rb:9:in `
': invalid argument: -x baz (OptionParser::InvalidArgument) + explicit_hash_values.rb:9:in '
': invalid argument: -x baz (OptionParser::InvalidArgument) $ ruby explicit_hash_values.rb -y ["-y", nil] $ ruby explicit_hash_values.rb -y baz @@ -426,7 +449,7 @@ Executions: $ ruby explicit_hash_values.rb -y bat ["-y", 3] $ ruby explicit_hash_values.rb -y ba - explicit_hash_values.rb:9:in `
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) + explicit_hash_values.rb:9:in '
': ambiguous argument: -y ba (OptionParser::AmbiguousArgument) $ ruby explicit_hash_values.rb -y bam ["-y", nil] @@ -449,7 +472,7 @@ Executions: $ ruby matched_values.rb --xxx FOO ["--xxx", "FOO"] $ ruby matched_values.rb --xxx bar - matched_values.rb:6:in `
': invalid argument: --xxx bar (OptionParser::InvalidArgument) + matched_values.rb:6:in '
': invalid argument: --xxx bar (OptionParser::InvalidArgument) === Keyword Argument +into+ @@ -501,7 +524,7 @@ Executions: -y, --yyyYYY Short and long, required argument -z, --zzz [ZZZ] Short and long, optional argument $ ruby missing_options.rb --yyy FOO - missing_options.rb:11:in `
': Missing required options: [:xxx, :zzz] (RuntimeError) + missing_options.rb:11:in '
': Missing required options: [:xxx, :zzz] (RuntimeError) ==== Default Values for Options @@ -522,11 +545,11 @@ Executions: === Argument Converters An option can specify that its argument is to be converted -from the default \String to an instance of another class. +from the default +String+ to an instance of another class. There are a number of built-in converters. Example: File +date.rb+ -defines an option whose argument is to be converted to a \Date object. +defines an option whose argument is to be converted to a +Date+ object. The argument is converted by method Date#parse. :include: ruby/date.rb @@ -546,7 +569,7 @@ for both built-in and custom converters. === Help -\OptionParser makes automatically generated help text available. ++OptionParser+ makes automatically generated help text available. The help text consists of: @@ -614,49 +637,49 @@ Execution: === Top List and Base List -An \OptionParser object maintains a stack of \OptionParser::List objects, +An +OptionParser+ object maintains a stack of OptionParser::List objects, each of which has a collection of zero or more options. It is unlikely that you'll need to add or take away from that stack. The stack includes: -- The top list, given by \OptionParser#top. -- The base list, given by \OptionParser#base. +- The top list, given by OptionParser#top. +- The base list, given by OptionParser#base. -When \OptionParser builds its help text, the options in the top list +When +OptionParser+ builds its help text, the options in the top list precede those in the base list. -=== Defining Options +=== Methods for Defining Options Option-defining methods allow you to create an option, and also append/prepend it to the top list or append it to the base list. Each of these next three methods accepts a sequence of parameter arguments and a block, -creates an option object using method \Option#make_switch (see below), +creates an option object using method OptionParser#make_switch (see below), and returns the created option: -- \Method \OptionParser#define appends the created option to the top list. +- \Method OptionParser#define appends the created option to the top list. -- \Method \OptionParser#define_head prepends the created option to the top list. +- \Method OptionParser#define_head prepends the created option to the top list. -- \Method \OptionParser#define_tail appends the created option to the base list. +- \Method OptionParser#define_tail appends the created option to the base list. These next three methods are identical to the three above, except for their return values: -- \Method \OptionParser#on is identical to method \OptionParser#define, +- \Method OptionParser#on is identical to method OptionParser#define, except that it returns the parser object +self+. -- \Method \OptionParser#on_head is identical to method \OptionParser#define_head, +- \Method OptionParser#on_head is identical to method OptionParser#define_head, except that it returns the parser object +self+. -- \Method \OptionParser#on_tail is identical to method \OptionParser#define_tail, +- \Method OptionParser#on_tail is identical to method OptionParser#define_tail, except that it returns the parser object +self+. Though you may never need to call it directly, here's the core method for defining an option: -- \Method \OptionParser#make_switch accepts an array of parameters and a block. +- \Method OptionParser#make_switch accepts an array of parameters and a block. See {Parameters for New Options}[optparse/option_params.rdoc]. This method is unlike others here in that it: - Accepts an array of parameters; @@ -668,7 +691,7 @@ here's the core method for defining an option: === Parsing -\OptionParser has six instance methods for parsing. ++OptionParser+ has six instance methods for parsing. Three have names ending with a "bang" (!): @@ -699,9 +722,9 @@ Each of these methods: (see {Keyword Argument into}[#label-Keyword+Argument+into]). - Returns +argv+, possibly with some elements removed. -==== \Method parse! +==== \Method +parse!+ -\Method parse!: +\Method +parse!+: - Accepts an optional array of string arguments +argv+; if not given, +argv+ defaults to the value of OptionParser#default_argv, @@ -756,9 +779,9 @@ Processing ended by non-option found when +POSIXLY_CORRECT+ is defined: ["--xxx", true] Returned: ["input_file.txt", "output_file.txt", "-yyy", "FOO"] (Array) -==== \Method parse +==== \Method +parse+ -\Method parse: +\Method +parse+: - Accepts an array of string arguments _or_ zero or more string arguments. @@ -810,25 +833,25 @@ Processing ended by non-option found when +POSIXLY_CORRECT+ is defined: ["--xxx", true] Returned: ["input_file.txt", "output_file.txt", "-yyy", "FOO"] (Array) -==== \Method order! +==== \Method +order!+ Calling method OptionParser#order! gives exactly the same result as calling method OptionParser#parse! with environment variable +POSIXLY_CORRECT+ defined. -==== \Method order +==== \Method +order+ Calling method OptionParser#order gives exactly the same result as calling method OptionParser#parse with environment variable +POSIXLY_CORRECT+ defined. -==== \Method permute! +==== \Method +permute!+ Calling method OptionParser#permute! gives exactly the same result as calling method OptionParser#parse! with environment variable +POSIXLY_CORRECT+ _not_ defined. -==== \Method permute +==== \Method +permute+ Calling method OptionParser#permute gives exactly the same result as calling method OptionParser#parse with environment variable diff --git a/lib/optparse.rb b/lib/optparse.rb index dc2b8a0..ea6844b 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -7,7 +7,7 @@ # # See OptionParser for documentation. # - +require 'set' unless defined?(Set) #-- # == Developer Documentation (not for RDoc output) @@ -48,7 +48,7 @@ # # == OptionParser # -# === New to \OptionParser? +# === New to +OptionParser+? # # See the {Tutorial}[optparse/tutorial.rdoc]. # @@ -143,7 +143,7 @@ # Used: # # $ ruby optparse-test.rb -r -# optparse-test.rb:9:in `
': missing argument: -r (OptionParser::MissingArgument) +# optparse-test.rb:9:in '
': missing argument: -r (OptionParser::MissingArgument) # $ ruby optparse-test.rb -r my-library # You required my-library! # @@ -152,14 +152,14 @@ # OptionParser supports the ability to coerce command line arguments # into objects for us. # -# OptionParser comes with a few ready-to-use kinds of type +# OptionParser comes with a few ready-to-use kinds of type # coercion. They are: # -# - Date -- Anything accepted by +Date.parse+ -# - DateTime -- Anything accepted by +DateTime.parse+ -# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ -# - URI -- Anything accepted by +URI.parse+ -# - Shellwords -- Anything accepted by +Shellwords.shellwords+ +# - Date -- Anything accepted by +Date.parse+ (need to require +optparse/date+) +# - DateTime -- Anything accepted by +DateTime.parse+ (need to require +optparse/date+) +# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ (need to require +optparse/time+) +# - URI -- Anything accepted by +URI.parse+ (need to require +optparse/uri+) +# - Shellwords -- Anything accepted by +Shellwords.shellwords+ (need to require +optparse/shellwords+) # - String -- Any non-empty string # - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040) # - Float -- Any float. (e.g. 10, 3.14, -100E+13) @@ -236,7 +236,7 @@ # $ ruby optparse-test.rb --user 2 # # # $ ruby optparse-test.rb --user 3 -# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError) +# optparse-test.rb:15:in 'block in find_user': No User Found for id 3 (RuntimeError) # # === Store options to a Hash # @@ -425,7 +425,8 @@ # If you have any questions, file a ticket at http://bugs.ruby-lang.org. # class OptionParser - OptionParser::Version = "0.3.0" + # The version string + OptionParser::Version = "0.7.0.dev.2" # :stopdoc: NoArgument = [NO_ARGUMENT = :NONE, nil].freeze @@ -438,6 +439,8 @@ class OptionParser # and resolved against a list of acceptable values. # module Completion + # :nodoc: + def self.regexp(key, icase) Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase) end @@ -459,7 +462,11 @@ def self.candidate(key, icase = false, pat = nil, &block) candidates end - def candidate(key, icase = false, pat = nil) + def self.completable?(key) + String.try_convert(key) or defined?(key.id2name) + end + + def candidate(key, icase = false, pat = nil, &_) Completion.candidate(key, icase, pat, &method(:each)) end @@ -494,7 +501,6 @@ def convert(opt = nil, val = nil, *) end end - # # Map from option/keyword string to object with completion. # @@ -502,7 +508,6 @@ class OptionMap < Hash include Completion end - # # Individual switch class. Not important to the user. # @@ -510,6 +515,8 @@ class OptionMap < Hash # RequiredArgument, etc. # class Switch + # :nodoc: + attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block # @@ -542,11 +549,11 @@ def self.pattern def initialize(pattern = nil, conv = nil, short = nil, long = nil, arg = nil, - desc = ([] if short or long), block = nil, &_block) + desc = ([] if short or long), block = nil, values = nil, &_block) raise if Array === pattern block ||= _block - @pattern, @conv, @short, @long, @arg, @desc, @block = - pattern, conv, short, long, arg, desc, block + @pattern, @conv, @short, @long, @arg, @desc, @block, @values = + pattern, conv, short, long, arg, desc, block, values end # @@ -579,11 +586,15 @@ def parse_arg(arg) # :nodoc: # exception. # def conv_arg(arg, val = []) # :nodoc: + v, = *val if conv val = conv.call(*val) else val = proc {|v| v}.call(*val) end + if @values + @values.include?(val) or raise InvalidArgument, v + end return arg, block, val end private :conv_arg @@ -664,7 +675,7 @@ def compsys(sdone, ldone) # :nodoc: (sopts+lopts).each do |opt| # "(-x -c -r)-l[left justify]" - if /^--\[no-\](.+)$/ =~ opt + if /\A--\[no-\](.+)$/ =~ opt o = $1 yield("--#{o}", desc.join("")) yield("--no-#{o}", desc.join("")) @@ -697,6 +708,11 @@ def pretty_print(q) # :nodoc: q.object_group(self) {pretty_print_contents(q)} end + def omitted_argument(val) # :nodoc: + val.pop if val.size == 3 and val.last.nil? + val + end + # # Switch that takes no arguments. # @@ -710,10 +726,10 @@ def parse(arg, argv) conv_arg(arg) end - def self.incompatible_argument_styles(*) + def self.incompatible_argument_styles(*) # :nodoc: end - def self.pattern + def self.pattern # :nodoc: Object end @@ -730,7 +746,7 @@ class RequiredArgument < self # # Raises an exception if argument is not present. # - def parse(arg, argv) + def parse(arg, argv, &_) unless arg raise MissingArgument if argv.empty? arg = argv.shift @@ -755,7 +771,7 @@ def parse(arg, argv, &error) if arg conv_arg(*parse_arg(arg, &error)) else - conv_arg(arg) + omitted_argument conv_arg(arg) end end @@ -774,13 +790,14 @@ class PlacedArgument < self # def parse(arg, argv, &error) if !(val = arg) and (argv.empty? or /\A-./ =~ (val = argv[0])) - return nil, block, nil + return nil, block end opt = (val = parse_arg(val, &error))[1] val = conv_arg(*val) if opt and !arg argv.shift else + omitted_argument val val[0] = nil end val @@ -798,6 +815,8 @@ def pretty_head # :nodoc: # matching pattern and converter pair. Also provides summary feature. # class List + # :nodoc: + # Map from acceptable argument types to pattern and converter pairs. attr_reader :atype @@ -837,7 +856,7 @@ def pretty_print(q) # :nodoc: def accept(t, pat = /.*/m, &block) if pat pat.respond_to?(:match) or - raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2)) + raise TypeError, "has no 'match'", ParseError.filter_backtrace(caller(2)) else pat = t if t.respond_to?(:match) end @@ -1020,7 +1039,6 @@ def match(key) DefaultList.short['-'] = Switch::NoArgument.new {} DefaultList.long[''] = Switch::NoArgument.new {throw :terminate} - COMPSYS_HEADER = <<'XXX' # :nodoc: typeset -A opt_args @@ -1033,11 +1051,31 @@ def compsys(to, name = File.basename($0)) # :nodoc: to << "#compdef #{name}\n" to << COMPSYS_HEADER visit(:compsys, {}, {}) {|o, d| - to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n] + to << %Q[ "#{o}[#{d.gsub(/[\\\"\[\]]/, '\\\\\&')}]" \\\n] } to << " '*:file:_files' && return 0\n" end + def help_exit + if $stdout.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?}) + less = ENV["LESS"] + args = [{"LESS" => "#{less} -Fe"}, pager, "w"] + print = proc do |f| + f.puts help + rescue Errno::EPIPE + # pager terminated + end + if Process.respond_to?(:fork) and false + IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call($stdout)} + # unreachable + end + IO.popen(*args, &print) + else + puts help + end + exit + end + # # Default options for ARGV, which never appear in option summary. # @@ -1049,8 +1087,7 @@ def compsys(to, name = File.basename($0)) # :nodoc: # Officious['help'] = proc do |parser| Switch::NoArgument.new do |arg| - puts parser.help - exit + parser.help_exit end end @@ -1071,7 +1108,7 @@ def compsys(to, name = File.basename($0)) # :nodoc: # Officious['*-completion-zsh'] = proc do |parser| Switch::OptionalArgument.new do |arg| - parser.compsys(STDOUT, arg) + parser.compsys($stdout, arg) exit end end @@ -1084,7 +1121,7 @@ def compsys(to, name = File.basename($0)) # :nodoc: Switch::OptionalArgument.new do |pkg| if pkg begin - require 'optparse/version' + require_relative 'optparse/version' rescue LoadError else show_version(*pkg.split(/,/)) or @@ -1129,6 +1166,10 @@ def self.inc(arg, default = nil) default.to_i + 1 end end + + # + # See self.inc + # def inc(*args) self.class.inc(*args) end @@ -1167,11 +1208,19 @@ def add_officious # :nodoc: def terminate(arg = nil) self.class.terminate(arg) end + # + # See #terminate. + # def self.terminate(arg = nil) throw :terminate, arg end @stack = [DefaultList] + # + # Returns the global top option list. + # + # Do not use directly. + # def self.top() DefaultList end # @@ -1192,9 +1241,9 @@ def self.accept(*args, &blk) top.accept(*args, &blk) end # # Directs to reject specified class argument. # - # +t+:: Argument class specifier, any object including Class. + # +type+:: Argument class specifier, any object including Class. # - # reject(t) + # reject(type) # def reject(*args, &blk) top.reject(*args, &blk) end # @@ -1245,7 +1294,15 @@ def banner # to $0. # def program_name - @program_name || File.basename($0, '.*') + @program_name || strip_ext(File.basename($0)) + end + + private def strip_ext(name) # :nodoc: + exts = /#{ + require "rbconfig" + Regexp.union(*RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split(" ")) + }\z/o + name.sub(exts, "") end # for experimental cascading :-) @@ -1284,10 +1341,24 @@ def ver end end + # + # Shows warning message with the program name + # + # +mesg+:: Message, defaulted to +$!+. + # + # See Kernel#warn. + # def warn(mesg = $!) super("#{program_name}: #{mesg}") end + # + # Shows message with the program name then aborts. + # + # +mesg+:: Message, defaulted to +$!+. + # + # See Kernel#abort. + # def abort(mesg = $!) super("#{program_name}: #{mesg}") end @@ -1309,6 +1380,9 @@ def base # # Pushes a new List. # + # If a block is given, yields +self+ and returns the result of the + # block, otherwise returns +self+. + # def new @stack.push(List.new) if block_given? @@ -1407,6 +1481,7 @@ def make_switch(opts, block = nil) klass = nil q, a = nil has_arg = false + values = nil opts.each do |o| # argument class @@ -1420,7 +1495,7 @@ def make_switch(opts, block = nil) end # directly specified pattern(any object possible to match) - if (!(String === o || Symbol === o)) and o.respond_to?(:match) + if !Completion.completable?(o) and o.respond_to?(:match) pattern = notwice(o, pattern, 'pattern') if pattern.respond_to?(:convert) conv = pattern.method(:convert).to_proc @@ -1434,7 +1509,12 @@ def make_switch(opts, block = nil) case o when Proc, Method block = notwice(o, block, 'block') - when Array, Hash + when Array, Hash, Set + if Array === o + o, v = o.partition {|v,| Completion.completable?(v)} + values = notwice(v, values, 'values') unless v.empty? + next if o.empty? + end case pattern when CompletingHash when nil @@ -1444,11 +1524,13 @@ def make_switch(opts, block = nil) raise ArgumentError, "argument pattern given twice" end o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}} + when Range + values = notwice(o, values, 'values') when Module raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4)) when *ArgumentStyle.keys style = notwice(ArgumentStyle[o], style, 'style') - when /^--no-([^\[\]=\s]*)(.+)?/ + when /\A--no-([^\[\]=\s]*)(.+)?/ q, a = $1, $2 o = notwice(a ? Object : TrueClass, klass, 'type') not_pattern, not_conv = search(:atype, o) unless not_style @@ -1459,7 +1541,7 @@ def make_switch(opts, block = nil) (q = q.downcase).tr!('_', '-') long << "no-#{q}" nolong << q - when /^--\[no-\]([^\[\]=\s]*)(.+)?/ + when /\A--\[no-\]([^\[\]=\s]*)(.+)?/ q, a = $1, $2 o = notwice(a ? Object : TrueClass, klass, 'type') if a @@ -1472,7 +1554,7 @@ def make_switch(opts, block = nil) not_pattern, not_conv = search(:atype, FalseClass) unless not_style not_style = Switch::NoArgument nolong << "no-#{o}" - when /^--([^\[\]=\s]*)(.+)?/ + when /\A--([^\[\]=\s]*)(.+)?/ q, a = $1, $2 if a o = notwice(NilClass, klass, 'type') @@ -1482,7 +1564,7 @@ def make_switch(opts, block = nil) ldesc << "--#{q}" (o = q.downcase).tr!('_', '-') long << o - when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ + when /\A-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ q, a = $1, $2 o = notwice(Object, klass, 'type') if a @@ -1493,7 +1575,7 @@ def make_switch(opts, block = nil) end sdesc << "-#{q}" short << Regexp.new(q) - when /^-(.)(.+)?/ + when /\A-(.)(.+)?/ q, a = $1, $2 if a o = notwice(NilClass, klass, 'type') @@ -1502,7 +1584,7 @@ def make_switch(opts, block = nil) end sdesc << "-#{q}" short << q - when /^=/ + when /\A=/ style = notwice(default_style.guess(arg = o), style, 'style') default_pattern, conv = search(:atype, Object) unless default_pattern else @@ -1511,12 +1593,18 @@ def make_switch(opts, block = nil) end default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern + if Range === values and klass + unless (!values.begin or klass === values.begin) and + (!values.end or klass === values.end) + raise ArgumentError, "range does not match class" + end + end if !(short.empty? and long.empty?) if has_arg and default_style == Switch::NoArgument default_style = Switch::RequiredArgument end s = (style || default_style).new(pattern || default_pattern, - conv, sdesc, ldesc, arg, desc, block) + conv, sdesc, ldesc, arg, desc, block, values) elsif !block if style or pattern raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller) @@ -1525,13 +1613,19 @@ def make_switch(opts, block = nil) else short << pattern s = (style || default_style).new(pattern, - conv, nil, nil, arg, desc, block) + conv, nil, nil, arg, desc, block, values) end return s, short, long, (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style), nolong end + # ---- + # Option definition phase methods + # + # These methods are used to define options, or to construct an + # OptionParser instance in other words. + # :call-seq: # define(*params, &block) # @@ -1607,6 +1701,13 @@ def separator(string) top.append(string, nil, nil) end + # ---- + # Arguments parse phase methods + # + # These methods parse +argv+, convert, and store the results by + # calling handlers. As these methods do not modify +self+, +self+ + # can be frozen. + # # Parses command line arguments +argv+ in order. When a block is given, # each non-option argument is yielded. When optional +into+ keyword @@ -1616,21 +1717,21 @@ def separator(string) # # Returns the rest of +argv+ left unparsed. # - def order(*argv, into: nil, &nonopt) + def order(*argv, **keywords, &nonopt) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - order!(argv, into: into, &nonopt) + order!(argv, **keywords, &nonopt) end # # Same as #order, but removes switches destructively. # Non-option arguments remain in +argv+. # - def order!(argv = default_argv, into: nil, &nonopt) + def order!(argv = default_argv, into: nil, **keywords, &nonopt) setter = ->(name, val) {into[name.to_sym] = val} if into - parse_in_order(argv, setter, &nonopt) + parse_in_order(argv, setter, **keywords, &nonopt) end - def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: + def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc: opt, arg, val, rest = nil nonopt ||= proc {|a| throw :terminate, a} argv.unshift(arg) if arg = catch(:terminate) { @@ -1641,19 +1742,24 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: opt, rest = $1, $2 opt.tr!('_', '-') begin - sw, = complete(:long, opt, true) - if require_exact && !sw.long.include?(arg) - throw :terminate, arg unless raise_unknown - raise InvalidOption, arg + if exact + sw, = search(:long, opt) + else + sw, = complete(:long, opt, true) end rescue ParseError throw :terminate, arg unless raise_unknown raise $!.set_option(arg, true) + else + unless sw + throw :terminate, arg unless raise_unknown + raise InvalidOption, arg + end end begin opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)} - val = cb.call(val) if cb - setter.call(sw.switch_name, val) if setter + val = callback!(cb, 1, val) if cb + callback!(setter, 2, sw.switch_name, val) if setter rescue ParseError raise $!.set_option(arg, rest) end @@ -1671,7 +1777,7 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: val = arg.delete_prefix('-') has_arg = true rescue InvalidOption - raise if require_exact + raise if exact # if no short options match, try completion with long # options. sw, = complete(:long, opt) @@ -1691,8 +1797,8 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: end begin argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-') - val = cb.call(val) if cb - setter.call(sw.switch_name, val) if setter + val = callback!(cb, 1, val) if cb + callback!(setter, 2, sw.switch_name, val) if setter rescue ParseError raise $!.set_option(arg, arg.length > 2) end @@ -1718,6 +1824,19 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: end private :parse_in_order + # Calls callback with _val_. + def callback!(cb, max_arity, *args) # :nodoc: + args.compact! + + if (size = args.size) < max_arity and cb.to_proc.lambda? + (arity = cb.arity) < 0 and arity = (1-arity) + arity = max_arity if arity > max_arity + args[arity - 1] = nil if arity > size + end + cb.call(*args) + end + private :callback! + # # Parses command line arguments +argv+ in permutation mode and returns # list of non-option arguments. When optional +into+ keyword @@ -1725,18 +1844,18 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: # []= method (so it can be Hash, or OpenStruct, or other # similar object). # - def permute(*argv, into: nil) + def permute(*argv, **keywords) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - permute!(argv, into: into) + permute!(argv, **keywords) end # # Same as #permute, but removes switches destructively. # Non-option arguments remain in +argv+. # - def permute!(argv = default_argv, into: nil) + def permute!(argv = default_argv, **keywords) nonopts = [] - order!(argv, into: into, &nonopts.method(:<<)) + order!(argv, **keywords) {|nonopt| nonopts << nonopt} argv[0, 0] = nonopts argv end @@ -1748,20 +1867,20 @@ def permute!(argv = default_argv, into: nil) # values are stored there via []= method (so it can be Hash, # or OpenStruct, or other similar object). # - def parse(*argv, into: nil) + def parse(*argv, **keywords) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - parse!(argv, into: into) + parse!(argv, **keywords) end # # Same as #parse, but removes switches destructively. # Non-option arguments remain in +argv+. # - def parse!(argv = default_argv, into: nil) + def parse!(argv = default_argv, **keywords) if ENV.include?('POSIXLY_CORRECT') - order!(argv, into: into) + order!(argv, **keywords) else - permute!(argv, into: into) + permute!(argv, **keywords) end end @@ -1775,18 +1894,30 @@ def parse!(argv = default_argv, into: nil) # # params["bar"] = "x" # --bar x # # params["zot"] = "z" # --zot Z # - def getopts(*args) + # Option +symbolize_names+ (boolean) specifies whether returned Hash keys should be Symbols; defaults to +false+ (use Strings). + # + # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option", symbolize_names: true) + # # params[:a] = true # -a + # # params[:b] = "1" # -b1 + # # params[:foo] = "1" # --foo + # # params[:bar] = "x" # --bar x + # # params[:zot] = "z" # --zot Z + # + def getopts(*args, symbolize_names: false, **keywords) argv = Array === args.first ? args.shift : default_argv single_options, *long_options = *args result = {} + setter = (symbolize_names ? + ->(name, val) {result[name.to_sym] = val} + : ->(name, val) {result[name] = val}) single_options.scan(/(.)(:)?/) do |opt, val| if val - result[opt] = nil + setter[opt, nil] define("-#{opt} VAL") else - result[opt] = false + setter[opt, false] define("-#{opt}") end end if single_options @@ -1795,23 +1926,23 @@ def getopts(*args) arg, desc = arg.split(';', 2) opt, val = arg.split(':', 2) if val - result[opt] = val.empty? ? nil : val + setter[opt, (val unless val.empty?)] define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact) else - result[opt] = false + setter[opt, false] define("--#{opt}", *[desc].compact) end end - parse_in_order(argv, result.method(:[]=)) + parse_in_order(argv, setter, **keywords) result end # # See #getopts. # - def self.getopts(*args) - new.getopts(*args) + def self.getopts(*args, symbolize_names: false) + new.getopts(*args, symbolize_names: symbolize_names) end # @@ -1854,7 +1985,7 @@ def complete(typ, opt, icase = false, *pat) # :nodoc: visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw} } exc = ambiguous ? AmbiguousOption : InvalidOption - raise exc.new(opt, additional: self.method(:additional_message).curry[typ]) + raise exc.new(opt, additional: proc {|o| additional_message(typ, o)}) end private :complete @@ -1872,6 +2003,9 @@ def additional_message(typ, opt) DidYouMean.formatter.message_for(all_candidates & checker.correct(opt)) end + # + # Return candidates for +word+. + # def candidate(word) list = [] case word @@ -1913,26 +2047,34 @@ def candidate(word) # The optional +into+ keyword argument works exactly like that accepted in # method #parse. # - def load(filename = nil, into: nil) + def load(filename = nil, **keywords) unless filename basename = File.basename($0, '.*') - return true if load(File.expand_path(basename, '~/.options'), into: into) rescue nil + return true if load(File.expand_path("~/.options/#{basename}"), **keywords) rescue nil basename << ".options" + if !(xdg = ENV['XDG_CONFIG_HOME']) or xdg.empty? + # https://specifications.freedesktop.org/basedir-spec/latest/#variables + # + # If $XDG_CONFIG_HOME is either not set or empty, a default + # equal to $HOME/.config should be used. + xdg = ['~/.config', true] + end return [ - # XDG - ENV['XDG_CONFIG_HOME'], - '~/.config', + xdg, + *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR), # Haiku - '~/config/settings', - ].any? {|dir| + ['~/config/settings', true], + ].any? {|dir, expand| next if !dir or dir.empty? - load(File.expand_path(basename, dir), into: into) rescue nil + filename = File.join(dir, basename) + filename = File.expand_path(filename) if expand + load(filename, **keywords) rescue nil } end begin - parse(*File.readlines(filename, chomp: true), into: into) + parse(*File.readlines(filename, chomp: true), **keywords) true rescue Errno::ENOENT, Errno::ENOTDIR false @@ -1945,10 +2087,10 @@ def load(filename = nil, into: nil) # # +env+ defaults to the basename of the program. # - def environment(env = File.basename($0, '.*')) + def environment(env = File.basename($0, '.*'), **keywords) env = ENV[env] || ENV[env.upcase] or return require 'shellwords' - parse(*Shellwords.shellwords(env)) + parse(*Shellwords.shellwords(env), **keywords) end # @@ -2084,10 +2226,23 @@ def environment(env = File.basename($0, '.*')) f |= Regexp::IGNORECASE if /i/ =~ o f |= Regexp::MULTILINE if /m/ =~ o f |= Regexp::EXTENDED if /x/ =~ o - k = o.delete("imx") - k = nil if k.empty? + case o = o.delete("imx") + when "" + when "u" + s = s.encode(Encoding::UTF_8) + when "e" + s = s.encode(Encoding::EUC_JP) + when "s" + s = s.encode(Encoding::SJIS) + when "n" + f |= Regexp::NOENCODING + else + raise OptionParser::InvalidArgument, "unknown regexp option - #{o}" + end + else + s ||= all end - Regexp.new(s || all, f, k) + Regexp.new(s, f) end # @@ -2101,6 +2256,7 @@ class ParseError < RuntimeError # Reason which caused the error. Reason = 'parse error' + # :nodoc: def initialize(*args, additional: nil) @additional = additional @arg0, = args @@ -2120,9 +2276,10 @@ def recover(argv) argv end + DIR = File.join(__dir__, '') def self.filter_backtrace(array) unless $DEBUG - array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~)) + array.delete_if {|bt| bt.start_with?(DIR)} end array end @@ -2251,19 +2408,19 @@ def options # Parses +self+ destructively in order and returns +self+ containing the # rest arguments left unparsed. # - def order!(&blk) options.order!(self, &blk) end + def order!(**keywords, &blk) options.order!(self, **keywords, &blk) end # # Parses +self+ destructively in permutation mode and returns +self+ # containing the rest arguments left unparsed. # - def permute!() options.permute!(self) end + def permute!(**keywords) options.permute!(self, **keywords) end # # Parses +self+ destructively and returns +self+ containing the # rest arguments left unparsed. # - def parse!() options.parse!(self) end + def parse!(**keywords) options.parse!(self, **keywords) end # # Substitution of getopts is possible as follows. Also see @@ -2276,8 +2433,8 @@ def parse!() options.parse!(self) end # rescue OptionParser::ParseError # end # - def getopts(*args) - options.getopts(self, *args) + def getopts(*args, symbolize_names: false, **keywords) + options.getopts(self, *args, symbolize_names: symbolize_names, **keywords) end # @@ -2287,7 +2444,8 @@ def self.extend_object(obj) super obj.instance_eval {@optparse = nil} end - def initialize(*args) + + def initialize(*args) # :nodoc: super @optparse = nil end diff --git a/lib/optparse/ac.rb b/lib/optparse/ac.rb index 0953725..23fc740 100644 --- a/lib/optparse/ac.rb +++ b/lib/optparse/ac.rb @@ -1,7 +1,11 @@ # frozen_string_literal: false require_relative '../optparse' +# +# autoconf-like options. +# class OptionParser::AC < OptionParser + # :stopdoc: private def _check_ac_args(name, block) @@ -14,6 +18,7 @@ def _check_ac_args(name, block) end ARG_CONV = proc {|val| val.nil? ? true : val} + private_constant :ARG_CONV def _ac_arg_enable(prefix, name, help_string, block) _check_ac_args(name, block) @@ -29,16 +34,27 @@ def _ac_arg_enable(prefix, name, help_string, block) enable end + # :startdoc: + public + # Define --enable / --disable style option + # + # Appears as --enable-name in help message. def ac_arg_enable(name, help_string, &block) _ac_arg_enable("enable", name, help_string, block) end + # Define --enable / --disable style option + # + # Appears as --disable-name in help message. def ac_arg_disable(name, help_string, &block) _ac_arg_enable("disable", name, help_string, block) end + # Define --with / --without style option + # + # Appears as --with-name in help message. def ac_arg_with(name, help_string, &block) _check_ac_args(name, block) diff --git a/lib/optparse/kwargs.rb b/lib/optparse/kwargs.rb index 992aca4..59a2f61 100644 --- a/lib/optparse/kwargs.rb +++ b/lib/optparse/kwargs.rb @@ -7,12 +7,17 @@ class OptionParser # # :include: ../../doc/optparse/creates_option.rdoc # - def define_by_keywords(options, meth, **opts) - meth.parameters.each do |type, name| + # Defines options which set in to _options_ for keyword parameters + # of _method_. + # + # Parameters for each keywords are given as elements of _params_. + # + def define_by_keywords(options, method, **params) + method.parameters.each do |type, name| case type when :key, :keyreq op, cl = *(type == :key ? %w"[ ]" : ["", ""]) - define("--#{name}=#{op}#{name.upcase}#{cl}", *opts[name]) do |o| + define("--#{name}=#{op}#{name.upcase}#{cl}", *params[name]) do |o| options[name] = o end end diff --git a/lib/optparse/version.rb b/lib/optparse/version.rb index b869d8f..b5ed695 100644 --- a/lib/optparse/version.rb +++ b/lib/optparse/version.rb @@ -2,6 +2,11 @@ # OptionParser internal utility class << OptionParser + # + # Shows version string in packages if Version is defined. + # + # +pkgs+:: package list + # def show_version(*pkgs) progname = ARGV.options.program_name result = false @@ -47,6 +52,8 @@ def show_version(*pkgs) result end + # :stopdoc: + def each_const(path, base = ::Object) path.split(/::|\//).inject(base) do |klass, name| raise NameError, path unless Module === klass @@ -68,4 +75,6 @@ def search_const(klass, name) end end end + + # :startdoc: end diff --git a/optparse.gemspec b/optparse.gemspec index a4287dd..6ea6b88 100644 --- a/optparse.gemspec +++ b/optparse.gemspec @@ -14,7 +14,10 @@ Gem::Specification.new do |spec| spec.email = ["nobu@ruby-lang.org"] spec.summary = %q{OptionParser is a class for command-line option analysis.} - spec.description = %q{OptionParser is a class for command-line option analysis.} + spec.description = File.open(File.join(__dir__, "README.md")) do |readme| + readme.gets("") # heading + readme.gets("").chomp + end rescue spec.summary spec.homepage = "https://github.com/ruby/optparse" spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0") spec.licenses = ["Ruby", "BSD-2-Clause"] @@ -22,8 +25,9 @@ Gem::Specification.new do |spec| spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage - spec.files = Dir["{doc,lib,misc}/**/*"] + %w[README.md ChangeLog COPYING] - spec.rdoc_options = ["--main=README.md", "--op=rdoc", "--page-dir=doc"] + dir, gemspec = File.split(__FILE__) + excludes = %W[#{gemspec} rakelib test/ Gemfile Rakefile .git* .editor*].map {|n| ":^"+n} + spec.files = IO.popen(%w[git ls-files -z --] + excludes, chdir: dir, &:read).split("\x0") spec.bindir = "exe" spec.executables = [] spec.require_paths = ["lib"] diff --git a/rakelib/changelogs.rake b/rakelib/changelogs.rake index df72f9d..9be2e8a 100644 --- a/rakelib/changelogs.rake +++ b/rakelib/changelogs.rake @@ -3,7 +3,10 @@ task "build" => "changelogs" changelog = proc do |output, ver = nil, prev = nil| ver &&= Gem::Version.new(ver) range = [[prev], [ver, "HEAD"]].map {|ver, branch| ver ? "v#{ver.to_s}" : branch}.compact.join("..") - IO.popen(%W[git log --format=fuller --topo-order --no-merges #{range}]) do |log| + cmd = %W[git log --date=iso --format=fuller --topo-order --no-merges + --invert-grep --fixed-strings --grep=#{'[ci skip]'} + #{range} --] + IO.popen(cmd) do |log| line = log.gets FileUtils.mkpath(File.dirname(output)) File.open(output, "wb") do |f| @@ -18,10 +21,13 @@ changelog = proc do |output, ver = nil, prev = nil| end tags = IO.popen(%w[git tag -l v[0-9]*]).grep(/v(.*)/) {$1} -tags.sort_by! {|tag| tag.scan(/\d+/).map(&:to_i)} -tags.inject(nil) do |prev, tag| - task("logs/ChangeLog-#{tag}") {|t| changelog[t.name, tag, prev]} - tag +unless tags.empty? + tags.sort_by! {|tag| tag.scan(/\d+/).map(&:to_i)} + tags.pop if IO.popen(%W[git rev-list --right-only --count v#{tags.last}..HEAD --], &:read).to_i == 0 + tags.inject(nil) do |prev, tag| + task("logs/ChangeLog-#{tag}") {|t| changelog[t.name, tag, prev]} + tag + end end desc "Make ChangeLog" diff --git a/rakelib/version.rake b/rakelib/version.rake index f3557a7..583530d 100644 --- a/rakelib/version.rake +++ b/rakelib/version.rake @@ -24,20 +24,36 @@ class << (helper = Bundler::GemHelper.instance) update_version commit_bump end + + def bump(major, minor = 0, teeny = 0, pre: nil) + self.version = [major, minor, teeny, pre].compact.join(".") + end + + def next_prerelease(*prefix, num) + if num + [*prefix, num.succ] + else + "dev.1" + end + end end -major, minor, teeny = helper.gemspec.version.segments +major, minor, teeny, *prerelease = helper.gemspec.version.segments + +task "bump:dev", [:pre] do |t, pre: helper.next_prerelease(*prerelease)| + helper.bump(major, minor, teeny, pre: pre) +end -task "bump:teeny" do - helper.version = Gem::Version.new("#{major}.#{minor}.#{teeny+1}") +task "bump:teeny", [:pre] do |t, pre: nil| + helper.bump(major, minor, teeny+1, pre: pre) end -task "bump:minor" do - helper.version = Gem::Version.new("#{major}.#{minor+1}.0") +task "bump:minor", [:pre] do |t, pre: nil| + helper.bump(major, minor+1, pre: pre) end -task "bump:major" do - helper.version = Gem::Version.new("#{major+1}.0.0") +task "bump:major", [:pre] do |t, pre: nil| + helper.bump(major+1, pre: pre) end task "bump" => "bump:teeny" diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb deleted file mode 100644 index bac3856..0000000 --- a/test/lib/core_assertions.rb +++ /dev/null @@ -1,768 +0,0 @@ -# frozen_string_literal: true - -module Test - module Unit - module Assertions - def _assertions= n # :nodoc: - @_assertions = n - end - - def _assertions # :nodoc: - @_assertions ||= 0 - end - - ## - # Returns a proc that will output +msg+ along with the default message. - - def message msg = nil, ending = nil, &default - proc { - msg = msg.call.chomp(".") if Proc === msg - custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? - "#{custom_message}#{default.call}#{ending || "."}" - } - end - end - - module CoreAssertions - require_relative 'envutil' - require 'pp' - - def mu_pp(obj) #:nodoc: - obj.pretty_inspect.chomp - end - - def assert_file - AssertFile - end - - FailDesc = proc do |status, message = "", out = ""| - now = Time.now - proc do - EnvUtil.failure_description(status, now, message, out) - end - end - - def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, - success: nil, **opt) - args = Array(args).dup - args.insert((Hash === args[0] ? 1 : 0), '--disable=gems') - stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) - desc = FailDesc[status, message, stderr] - if block_given? - raise "test_stdout ignored, use block only or without block" if test_stdout != [] - raise "test_stderr ignored, use block only or without block" if test_stderr != [] - yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) - else - all_assertions(desc) do |a| - [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| - a.for(key) do - if exp.is_a?(Regexp) - assert_match(exp, act) - elsif exp.all? {|e| String === e} - assert_equal(exp, act.lines.map {|l| l.chomp }) - else - assert_pattern_list(exp, act) - end - end - end - unless success.nil? - a.for("success?") do - if success - assert_predicate(status, :success?) - else - assert_not_predicate(status, :success?) - end - end - end - end - status - end - end - - if defined?(RubyVM::InstructionSequence) - def syntax_check(code, fname, line) - code = code.dup.force_encoding(Encoding::UTF_8) - RubyVM::InstructionSequence.compile(code, fname, fname, line) - :ok - ensure - raise if SyntaxError === $! - end - else - def syntax_check(code, fname, line) - code = code.b - code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { - "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" - } - code = code.force_encoding(Encoding::UTF_8) - catch {|tag| eval(code, binding, fname, line - 1)} - end - end - - def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) - # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail - pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? - - require_relative 'memory_status' - raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) - - token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" - token_dump = token.dump - token_re = Regexp.quote(token) - envs = args.shift if Array === args and Hash === args.first - args = [ - "--disable=gems", - "-r", File.expand_path("../memory_status", __FILE__), - *args, - "-v", "-", - ] - if defined? Memory::NO_MEMORY_LEAK_ENVS then - envs ||= {} - newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } - envs = newenvs if newenvs - end - args.unshift(envs) if envs - cmd = [ - 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', - prepare, - 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', - '$initial_size = $initial_status.size', - code, - 'GC.start', - ].join("\n") - _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) - before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) - after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) - assert(status.success?, FailDesc[status, message, err]) - ([:size, (rss && :rss)] & after.members).each do |n| - b = before[n] - a = after[n] - next unless a > 0 and b > 0 - assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) - end - rescue LoadError - pend - end - - # :call-seq: - # assert_nothing_raised( *args, &block ) - # - #If any exceptions are given as arguments, the assertion will - #fail if one of those exceptions are raised. Otherwise, the test fails - #if any exceptions are raised. - # - #The final argument may be a failure message. - # - # assert_nothing_raised RuntimeError do - # raise Exception #Assertion passes, Exception is not a RuntimeError - # end - # - # assert_nothing_raised do - # raise Exception #Assertion fails - # end - def assert_nothing_raised(*args) - self._assertions += 1 - if Module === args.last - msg = nil - else - msg = args.pop - end - begin - line = __LINE__; yield - rescue Test::Unit::PendedError - raise - rescue Exception => e - bt = e.backtrace - as = e.instance_of?(Test::Unit::AssertionFailedError) - if as - ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o - bt.reject! {|ln| ans =~ ln} - end - if ((args.empty? && !as) || - args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) - msg = message(msg) { - "Exception raised:\n<#{mu_pp(e)}>\n" + - "Backtrace:\n" + - e.backtrace.map{|frame| " #{frame}"}.join("\n") - } - raise Test::Unit::AssertionFailedError, msg.call, bt - else - raise - end - end - end - - def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) - fname ||= caller_locations(2, 1)[0] - mesg ||= fname.to_s - verbose, $VERBOSE = $VERBOSE, verbose - case - when Array === fname - fname, line = *fname - when defined?(fname.path) && defined?(fname.lineno) - fname, line = fname.path, fname.lineno - else - line = 1 - end - yield(code, fname, line, message(mesg) { - if code.end_with?("\n") - "```\n#{code}```\n" - else - "```\n#{code}\n```\n""no-newline" - end - }) - ensure - $VERBOSE = verbose - end - - def assert_valid_syntax(code, *args, **opt) - prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| - yield if defined?(yield) - assert_nothing_raised(SyntaxError, mesg) do - assert_equal(:ok, syntax_check(src, fname, line), mesg) - end - end - end - - def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) - assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) - if child_env - child_env = [child_env] - else - child_env = [] - end - out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) - assert !status.signaled?, FailDesc[status, message, out] - end - - def assert_ruby_status(args, test_stdin="", message=nil, **opt) - out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) - desc = FailDesc[status, message, out] - assert(!status.signaled?, desc) - message ||= "ruby exit status is not success:" - assert(status.success?, desc) - end - - ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") - - def separated_runner(out = nil) - include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) - out = out ? IO.new(out, 'w') : STDOUT - at_exit { - out.puts [Marshal.dump($!)].pack('m'), "assertions=#{self._assertions}" - } - Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) - end - - def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) - unless file and line - loc, = caller_locations(1,1) - file ||= loc.path - line ||= loc.lineno - end - capture_stdout = true - unless /mswin|mingw/ =~ RUBY_PLATFORM - capture_stdout = false - opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner) - res_p, res_c = IO.pipe - opt[:ios] = [res_c] - end - src = < marshal_error - ignore_stderr = nil - res = nil - end - if res and !(SystemExit === res) - if bt = res.backtrace - bt.each do |l| - l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} - end - bt.concat(caller) - else - res.set_backtrace(caller) - end - raise res - end - - # really is it succeed? - unless ignore_stderr - # the body of assert_separately must not output anything to detect error - assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) - end - assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) - raise marshal_error if marshal_error - end - - # Run Ractor-related test without influencing the main test suite - def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) - return unless defined?(Ractor) - - require = "require #{require.inspect}" if require - if require_relative - dir = File.dirname(caller_locations[0,1][0].absolute_path) - full_path = File.expand_path(require_relative, dir) - require = "#{require}; require #{full_path.inspect}" - end - - assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) - #{require} - previous_verbose = $VERBOSE - $VERBOSE = nil - Ractor.new {} # trigger initial warning - $VERBOSE = previous_verbose - #{src} - RUBY - end - - # :call-seq: - # assert_throw( tag, failure_message = nil, &block ) - # - #Fails unless the given block throws +tag+, returns the caught - #value otherwise. - # - #An optional failure message may be provided as the final argument. - # - # tag = Object.new - # assert_throw(tag, "#{tag} was not thrown!") do - # throw tag - # end - def assert_throw(tag, msg = nil) - ret = catch(tag) do - begin - yield(tag) - rescue UncaughtThrowError => e - thrown = e.tag - end - msg = message(msg) { - "Expected #{mu_pp(tag)} to have been thrown"\ - "#{%Q[, not #{thrown}] if thrown}" - } - assert(false, msg) - end - assert(true) - ret - end - - # :call-seq: - # assert_raise( *args, &block ) - # - #Tests if the given block raises an exception. Acceptable exception - #types may be given as optional arguments. If the last argument is a - #String, it will be used as the error message. - # - # assert_raise do #Fails, no Exceptions are raised - # end - # - # assert_raise NameError do - # puts x #Raises NameError, so assertion succeeds - # end - def assert_raise(*exp, &b) - case exp.last - when String, Proc - msg = exp.pop - end - - begin - yield - rescue Test::Unit::PendedError => e - return e if exp.include? Test::Unit::PendedError - raise e - rescue Exception => e - expected = exp.any? { |ex| - if ex.instance_of? Module then - e.kind_of? ex - else - e.instance_of? ex - end - } - - assert expected, proc { - flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) - } - - return e - ensure - unless e - exp = exp.first if exp.size == 1 - - flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) - end - end - end - - # :call-seq: - # assert_raise_with_message(exception, expected, msg = nil, &block) - # - #Tests if the given block raises an exception with the expected - #message. - # - # assert_raise_with_message(RuntimeError, "foo") do - # nil #Fails, no Exceptions are raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise ArgumentError, "foo" #Fails, different Exception is raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "bar" #Fails, RuntimeError is raised but the message differs - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "foo" #Raises RuntimeError with the message, so assertion succeeds - # end - def assert_raise_with_message(exception, expected, msg = nil, &block) - case expected - when String - assert = :assert_equal - when Regexp - assert = :assert_match - else - raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" - end - - ex = m = nil - EnvUtil.with_default_internal(expected.encoding) do - ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do - yield - end - m = ex.message - end - msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} - - if assert == :assert_equal - assert_equal(expected, m, msg) - else - msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } - assert expected =~ m, msg - block.binding.eval("proc{|_|$~=_}").call($~) - end - ex - end - - MINI_DIR = File.join(File.dirname(File.expand_path(__FILE__)), "minitest") #:nodoc: - - # :call-seq: - # assert(test, [failure_message]) - # - #Tests if +test+ is true. - # - #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used - #as the failure message. Otherwise, the result of calling +msg+ will be - #used as the message if the assertion fails. - # - #If no +msg+ is given, a default message will be used. - # - # assert(false, "This was expected to be true") - def assert(test, *msgs) - case msg = msgs.first - when String, Proc - when nil - msgs.shift - else - bt = caller.reject { |s| s.start_with?(MINI_DIR) } - raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt - end unless msgs.empty? - super - end - - # :call-seq: - # assert_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object responds to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_respond_to("hello", :reverse) #Succeeds - # assert_respond_to("hello", :does_not_exist) #Fails - def assert_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" - } - return assert obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return if obj.respond_to?(meth) - end - super(obj, meth, msg) - end - - # :call-seq: - # assert_not_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object does not respond to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_not_respond_to("hello", :reverse) #Fails - # assert_not_respond_to("hello", :does_not_exist) #Succeeds - def assert_not_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" - } - return assert !obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return unless obj.respond_to?(meth) - end - refute_respond_to(obj, meth, msg) - end - - # pattern_list is an array which contains regexp and :*. - # :* means any sequence. - # - # pattern_list is anchored. - # Use [:*, regexp, :*] for non-anchored match. - def assert_pattern_list(pattern_list, actual, message=nil) - rest = actual - anchored = true - pattern_list.each_with_index {|pattern, i| - if pattern == :* - anchored = false - else - if anchored - match = /\A#{pattern}/.match(rest) - else - match = pattern.match(rest) - end - unless match - msg = message(msg) { - expect_msg = "Expected #{mu_pp pattern}\n" - if /\n[^\n]/ =~ rest - actual_mesg = +"to match\n" - rest.scan(/.*\n+/) { - actual_mesg << ' ' << $&.inspect << "+\n" - } - actual_mesg.sub!(/\+\n\z/, '') - else - actual_mesg = "to match " + mu_pp(rest) - end - actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" - expect_msg + actual_mesg - } - assert false, msg - end - rest = match.post_match - anchored = true - end - } - if anchored - assert_equal("", rest) - end - end - - def assert_warning(pat, msg = nil) - result = nil - stderr = EnvUtil.with_default_internal(pat.encoding) { - EnvUtil.verbose_warning { - result = yield - } - } - msg = message(msg) {diff pat, stderr} - assert(pat === stderr, msg) - result - end - - def assert_warn(*args) - assert_warning(*args) {$VERBOSE = false; yield} - end - - def assert_deprecated_warning(mesg = /deprecated/) - assert_warning(mesg) do - Warning[:deprecated] = true - yield - end - end - - def assert_deprecated_warn(mesg = /deprecated/) - assert_warn(mesg) do - Warning[:deprecated] = true - yield - end - end - - class << (AssertFile = Struct.new(:failure_message).new) - include Assertions - include CoreAssertions - def assert_file_predicate(predicate, *args) - if /\Anot_/ =~ predicate - predicate = $' - neg = " not" - end - result = File.__send__(predicate, *args) - result = !result if neg - mesg = "Expected file ".dup << args.shift.inspect - mesg << "#{neg} to be #{predicate}" - mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? - mesg << " #{failure_message}" if failure_message - assert(result, mesg) - end - alias method_missing assert_file_predicate - - def for(message) - clone.tap {|a| a.failure_message = message} - end - end - - class AllFailures - attr_reader :failures - - def initialize - @count = 0 - @failures = {} - end - - def for(key) - @count += 1 - yield - rescue Exception => e - @failures[key] = [@count, e] - end - - def foreach(*keys) - keys.each do |key| - @count += 1 - begin - yield key - rescue Exception => e - @failures[key] = [@count, e] - end - end - end - - def message - i = 0 - total = @count.to_s - fmt = "%#{total.size}d" - @failures.map {|k, (n, v)| - v = v.message - "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" - }.join("\n") - end - - def pass? - @failures.empty? - end - end - - # threads should respond to shift method. - # Array can be used. - def assert_join_threads(threads, message = nil) - errs = [] - values = [] - while th = threads.shift - begin - values << th.value - rescue Exception - errs << [th, $!] - th = nil - end - end - values - ensure - if th&.alive? - th.raise(Timeout::Error.new) - th.join rescue errs << [th, $!] - end - if !errs.empty? - msg = "exceptions on #{errs.length} threads:\n" + - errs.map {|t, err| - "#{t.inspect}:\n" + - RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message - }.join("\n---\n") - if message - msg = "#{message}\n#{msg}" - end - raise Test::Unit::AssertionFailedError, msg - end - end - - def assert_all?(obj, m = nil, &blk) - failed = [] - obj.each do |*a, &b| - unless blk.call(*a, &b) - failed << (a.size > 1 ? a : a[0]) - end - end - assert(failed.empty?, message(m) {failed.pretty_inspect}) - end - - def assert_all_assertions(msg = nil) - all = AllFailures.new - yield all - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions assert_all_assertions - - def assert_all_assertions_foreach(msg = nil, *keys, &block) - all = AllFailures.new - all.foreach(*keys, &block) - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions_foreach assert_all_assertions_foreach - - def message(msg = nil, *args, &default) # :nodoc: - if Proc === msg - super(nil, *args) do - ary = [msg.call, (default.call if default)].compact.reject(&:empty?) - if 1 < ary.length - ary[0...-1] = ary[0...-1].map {|str| str.sub(/(?@tmpdir}) optdir = File.join(@tmpdir, dir) FileUtils.mkdir_p(optdir) file = File.join(optdir, [@basename, suffix].join("")) @@ -41,7 +47,7 @@ def setup_options(env, dir, suffix = nil) begin yield dir, optdir ensure - File.unlink(file) + File.unlink(file) rescue nil Dir.rmdir(optdir) rescue nil end else @@ -50,7 +56,7 @@ def setup_options(env, dir, suffix = nil) end def setup_options_home(&block) - setup_options({'HOME'=>@tmpdir}, ".options", &block) + setup_options({}, ".options", &block) end def setup_options_xdg_config_home(&block) @@ -58,7 +64,7 @@ def setup_options_xdg_config_home(&block) end def setup_options_home_config(&block) - setup_options({'HOME'=>@tmpdir}, ".config", ".options", &block) + setup_options({}, ".config", ".options", &block) end def setup_options_xdg_config_dirs(&block) @@ -66,7 +72,11 @@ def setup_options_xdg_config_dirs(&block) end def setup_options_home_config_settings(&block) - setup_options({'HOME'=>@tmpdir}, "config/settings", ".options", &block) + setup_options({}, "config/settings", ".options", &block) + end + + def setup_options_home_options(envname, &block) + setup_options({envname => '~/options'}, "options", ".options", &block) end def test_load_home_options @@ -91,7 +101,7 @@ def test_load_home_options end def test_load_xdg_config_home - result, = setup_options_xdg_config_home + result, dir = setup_options_xdg_config_home assert_load(result) setup_options_home_config do @@ -105,6 +115,11 @@ def test_load_xdg_config_home setup_options_home_config_settings do assert_load(result) end + + File.unlink("#{dir}/#{@basename}.options") + setup_options_home_config do + assert_load_nothing + end end def test_load_home_config @@ -118,6 +133,11 @@ def test_load_home_config setup_options_home_config_settings do assert_load(result) end + + setup_options_xdg_config_home do |_, dir| + File.unlink("#{dir}/#{@basename}.options") + assert_load_nothing + end end def test_load_xdg_config_dirs @@ -135,7 +155,34 @@ def test_load_home_config_settings end def test_load_nothing - assert !new_parser.load - assert_nil @result + setup_options({}, "") do + assert_load_nothing + end + end + + def test_not_expand_path_basename + basename = @basename + @basename = "~" + $test_optparse_basename = "/" + @basename + alias $test_optparse_prog $0 + alias $0 $test_optparse_basename + setup_options({'HOME'=>@tmpdir+"/~options"}, "", "options") do + assert_load_nothing + end + ensure + alias $0 $test_optparse_prog + @basename = basename + end + + def test_not_expand_path_xdg_config_home + setup_options_home_options('XDG_CONFIG_HOME') do + assert_load_nothing + end + end + + def test_not_expand_path_xdg_config_dirs + setup_options_home_options('XDG_CONFIG_DIRS') do + assert_load_nothing + end end end diff --git a/test/optparse/test_optarg.rb b/test/optparse/test_optarg.rb index 81127a8..f944605 100644 --- a/test/optparse/test_optarg.rb +++ b/test/optparse/test_optarg.rb @@ -9,6 +9,8 @@ def setup @opt.def_option("--regexp[=REGEXP]", Regexp) {|x| @reopt = x} @opt.def_option "--with_underscore[=VAL]" do |x| @flag = x end @opt.def_option "--with-hyphen[=VAL]" do |x| @flag = x end + @opt.def_option("--fallback[=VAL]") do |x = "fallback"| @flag = x end + @opt.def_option("--lambda[=VAL]", &->(x) {@flag = x}) @reopt = nil end @@ -57,4 +59,18 @@ def test_hyphenize assert_equal(%w"", no_error {@opt.parse!(%w"--with_hyphen=foo4")}) assert_equal("foo4", @flag) end + + def test_default_argument + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback=val1")}) + assert_equal("val1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback")}) + assert_equal("fallback", @flag) + end + + def test_lambda + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda=lambda1")}) + assert_equal("lambda1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda")}) + assert_equal(nil, @flag) + end end diff --git a/test/optparse/test_optparse.rb b/test/optparse/test_optparse.rb index 5a0593d..ff33400 100644 --- a/test/optparse/test_optparse.rb +++ b/test/optparse/test_optparse.rb @@ -63,6 +63,9 @@ def test_regexp assert_equal(/foo/i, @reopt) assert_equal(%w"", no_error {@opt.parse!(%w"--regexp=/foo/n")}) assert_equal(/foo/n, @reopt) + assert_equal(%w"", no_error {@opt.parse!(%W"--regexp=/\u{3042}/s")}) + assert_equal(Encoding::Windows_31J, @reopt.encoding) + assert_equal("\x82\xa0".force_encoding(Encoding::Windows_31J), @reopt.source) end def test_into @@ -70,10 +73,30 @@ def test_into @opt.def_option "-p", "--port=PORT", "port", Integer @opt.def_option "-v", "--verbose" do @verbose = true end @opt.def_option "-q", "--quiet" do @quiet = true end + @opt.def_option "-o", "--option [OPT]" do |opt| @option = opt end + @opt.def_option "-a", "--array [VAL]", Array do |val| val end result = {} @opt.parse %w(--host localhost --port 8000 -v), into: result assert_equal({host: "localhost", port: 8000, verbose: true}, result) assert_equal(true, @verbose) + result = {} + @opt.parse %w(--option -q), into: result + assert_equal({quiet: true, option: nil}, result) + result = {} + @opt.parse %w(--option OPTION -v), into: result + assert_equal({verbose: true, option: "OPTION"}, result) + result = {} + @opt.parse %w(-a b,c,d), into: result + assert_equal({array: %w(b c d)}, result) + result = {} + @opt.parse %w(--array b,c,d), into: result + assert_equal({array: %w(b c d)}, result) + result = {} + @opt.parse %w(-a b), into: result + assert_equal({array: %w(b)}, result) + result = {} + @opt.parse %w(--array b), into: result + assert_equal({array: %w(b)}, result) end def test_require_exact @@ -85,9 +108,9 @@ def test_require_exact end @opt.require_exact = true - %w(--zrs -F -Ffoo).each do |arg| + [%w(--zrs foo), %w(--zrs=foo), %w(-F foo), %w(-Ffoo)].each do |args| result = {} - @opt.parse([arg, 'foo'], into: result) + @opt.parse(args, into: result) assert_equal({zrs: 'foo'}, result) end @@ -96,6 +119,43 @@ def test_require_exact assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zrs foo))} assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zr foo))} assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-z foo))} + + @opt.def_option('-f', '--[no-]foo', 'foo') {|arg| @foo = arg} + @opt.parse(%w[-f]) + assert_equal(true, @foo) + @opt.parse(%w[--foo]) + assert_equal(true, @foo) + @opt.parse(%w[--no-foo]) + assert_equal(false, @foo) + end + + def test_exact_option + @opt.def_option('-F', '--zrs=IRS', 'zrs') + %w(--zrs --zr --z -zfoo -z -F -Ffoo).each do |arg| + result = {} + @opt.parse([arg, 'foo'], into: result) + assert_equal({zrs: 'foo'}, result) + end + + [%w(--zrs foo), %w(--zrs=foo), %w(-F foo), %w(-Ffoo)].each do |args| + result = {} + @opt.parse(args, into: result, exact: true) + assert_equal({zrs: 'foo'}, result) + end + + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(--zr foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(--z foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zrs foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zr foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-z foo), exact: true)} + + @opt.def_option('-f', '--[no-]foo', 'foo') {|arg| @foo = arg} + @opt.parse(%w[-f], exact: true) + assert_equal(true, @foo) + @opt.parse(%w[--foo], exact: true) + assert_equal(true, @foo) + @opt.parse(%w[--no-foo], exact: true) + assert_equal(false, @foo) end def test_raise_unknown @@ -117,4 +177,55 @@ def test_nonopt_pattern e = assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-t))} assert_equal(["-t"], e.args) end + + def test_help_pager + require 'tmpdir' + Dir.mktmpdir do |dir| + File.open(File.join(dir, "options.rb"), "w") do |f| + f.puts "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + stdout = $stdout.dup + def stdout.tty?; true; end + $stdout = stdout + ARGV.options do |opt| + end; + 100.times {|i| f.puts " opt.on('--opt-#{i}') {}"} + f.puts "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + opt.parse! + end + end; + end + + optparse = $".find {|path| path.end_with?("/optparse.rb")} + args = ["-r#{optparse}", "options.rb", "--help"] + cmd = File.join(dir, "pager.cmd") + if RbConfig::CONFIG["EXECUTABLE_EXTS"]&.include?(".cmd") + command = "@echo off" + else # if File.executable?("/bin/sh") + # TruffleRuby just calls `posix_spawnp` and no fallback to `/bin/sh`. + command = "#!/bin/sh\n" + end + + [ + [{"RUBY_PAGER"=>cmd, "PAGER"=>"echo ng"}, "Executing RUBY_PAGER"], + [{"RUBY_PAGER"=>nil, "PAGER"=>cmd}, "Executing PAGER"], + ].each do |env, expected| + File.write(cmd, "#{command}\n" "echo #{expected}\n", perm: 0o700) + assert_in_out_err([env, *args], "", [expected], chdir: dir) + end + end + end + + def test_program_name + program = $0 + $0 = "rdbg3.5" + assert_equal "rdbg3.5", OptionParser.new.program_name + RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split(" ") do |ext| + $0 = "rdbg3.5" + ext + assert_equal "rdbg3.5", OptionParser.new.program_name + end + ensure + $0 = program + end end diff --git a/test/optparse/test_placearg.rb b/test/optparse/test_placearg.rb index ed0e4d3..d5be5a6 100644 --- a/test/optparse/test_placearg.rb +++ b/test/optparse/test_placearg.rb @@ -7,12 +7,18 @@ def setup @opt.def_option("-x [VAL]") {|x| @flag = x} @opt.def_option("--option [VAL]") {|x| @flag = x} @opt.def_option("-T [level]", /^[0-4]$/, Integer) {|x| @topt = x} + @opt.def_option("--enum [VAL]", [:Alpha, :Bravo, :Charlie]) {|x| @enum = x} + @opt.def_option("--enumval [VAL]", [[:Alpha, 1], [:Bravo, 2], [:Charlie, 3]]) {|x| @enum = x} + @opt.def_option("--integer [VAL]", Integer, [1, 2, 3]) {|x| @integer = x} + @opt.def_option("--range [VAL]", Integer, 1..3) {|x| @range = x} @topt = nil @opt.def_option("-n") {} @opt.def_option("--regexp [REGEXP]", Regexp) {|x| @reopt = x} @reopt = nil @opt.def_option "--with_underscore=VAL" do |x| @flag = x end @opt.def_option "--with-hyphen=VAL" do |x| @flag = x end + @opt.def_option("--fallback [VAL]") do |x = "fallback"| @flag = x end + @opt.def_option("--lambda [VAL]", &->(x) {@flag = x}) end def test_short @@ -73,4 +79,43 @@ def test_conv assert_equal(%w"te.rb", no_error('[ruby-dev:38333]') {@opt.parse!(%w"-T1 te.rb")}) assert_equal(1, @topt) end + + def test_default_argument + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback=val1")}) + assert_equal("val1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback val2")}) + assert_equal("val2", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback")}) + assert_equal("fallback", @flag) + end + + def test_lambda + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda=lambda1")}) + assert_equal("lambda1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda lambda2")}) + assert_equal("lambda2", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda")}) + assert_equal(nil, @flag) + end + + def test_enum + assert_equal([], no_error {@opt.parse!(%w"--enum=A")}) + assert_equal(:Alpha, @enum) + end + + def test_enum_pair + assert_equal([], no_error {@opt.parse!(%w"--enumval=A")}) + assert_equal(1, @enum) + end + + def test_enum_conversion + assert_equal([], no_error {@opt.parse!(%w"--integer=1")}) + assert_equal(1, @integer) + end + + def test_enum_range + assert_equal([], no_error {@opt.parse!(%w"--range=1")}) + assert_equal(1, @range) + assert_raise(OptionParser::InvalidArgument) {@opt.parse!(%w"--range=4")} + end end diff --git a/test/optparse/test_reqarg.rb b/test/optparse/test_reqarg.rb index d5686d1..31d4fef 100644 --- a/test/optparse/test_reqarg.rb +++ b/test/optparse/test_reqarg.rb @@ -6,6 +6,7 @@ def setup super @opt.def_option "--with_underscore=VAL" do |x| @flag = x end @opt.def_option "--with-hyphen=VAL" do |x| @flag = x end + @opt.def_option("--lambda=VAL", &->(x) {@flag = x}) end class Def1 < TestOptionParser @@ -81,6 +82,11 @@ def test_hyphenize assert_equal("foo4", @flag) end + def test_lambda + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda=lambda1")}) + assert_equal("lambda1", @flag) + end + class TestOptionParser::WithPattern < TestOptionParser def test_pattern pat = num = nil diff --git a/test/optparse/test_switch.rb b/test/optparse/test_switch.rb new file mode 100644 index 0000000..b06f4e3 --- /dev/null +++ b/test/optparse/test_switch.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: false + +require 'test/unit' +require 'optparse' + + +class TestOptionParserSwitch < Test::Unit::TestCase + + def setup + @parser = OptionParser.new + end + + def assert_invalidarg_error(msg, &block) + exc = assert_raise(OptionParser::InvalidArgument) do + yield + end + assert_equal "invalid argument: #{msg}", exc.message + end + + def test_make_switch__enum_array + p = @parser + p.on("--enum=", ["aa", "bb", "cc"]) + p.permute(["--enum=bb"], into: (opts={})) + assert_equal({:enum=>"bb"}, opts) + assert_invalidarg_error("--enum=dd") do + p.permute(["--enum=dd"], into: (opts={})) + end + end + + def test_make_switch__enum_hash + p = @parser + p.on("--hash=", {"aa"=>"AA", "bb"=>"BB"}) + p.permute(["--hash=bb"], into: (opts={})) + assert_equal({:hash=>"BB"}, opts) + assert_invalidarg_error("--hash=dd") do + p.permute(["--hash=dd"], into: (opts={})) + end + end + + def test_make_switch__enum_set + p = @parser + p.on("--set=", Set.new(["aa", "bb", "cc"])) + p.permute(["--set=bb"], into: (opts={})) + assert_equal({:set=>"bb"}, opts) + assert_invalidarg_error("--set=dd") do + p.permute(["--set=dd"], into: (opts={})) + end + end + +end